1

I think it's a pretty conscious design decision to make the creation of a std::tuple through std::make_tuple require rvalue reference arguments (type T&&).

However, this implies move semantics (std::move is little more than a cast to T&&) for object types, and I'm not really comfortable with construction of a std::tuple always requiring that.

int x = 7, y = 5;
std::tuple<int, int> fraction = make_tuple<int, int>( x, y ); // fails

About the above, the compiler says:

error C2664: 'std::tuple<int,int> std::make_tuple<int,int>(int &&,int &&)': cannot convert argument 1 from 'int' to 'int &&'

message : You cannot bind an lvalue to an rvalue reference

You can make a std::tuple from lvalues no problem if you don't use make_tuple:

std::tuple<int, int> fraction = { x, y }; // ok

My question is, why is this?

bobobobo
  • 64,917
  • 62
  • 258
  • 363

1 Answers1

6

std::make_tuple doesn't take an rvalue reference to a T, contrary to what it seems; it takes a universal reference to a T (T&&). If universal reference is new to you, let me explain.

The definition of make_tuple looks more or less like this:

template<typename... Ts>
std::tuple<Ts...> make_tuple(Ts&&... ts){ 
    // ... 
}

But for purposes of explanation, I am going to refer to make_tuple like this:

template<typename T>
std::tuple<T> make_tuple(T&& t){ 
    // ... 
}

Using type deduction, when make_tuple is passed an rvalue (lets say an int&&), the type deduced for T is int, since make_tuple takes a T&& and it was passed an rvalue. The definition of make_tuple (with T deduced) now looks like this:

std::tuple<int> make_tuple(int&& t){ 
    // ... 
}

Here is where things get confusing: if make_tuple is passed an lvalue int, what should T be deduced as? The compiler deduces T as int& and uses something called reference collapsing.

Reference collapsing basically states that if the compiler forms a reference to a reference and one of them is lvalue, then the resulting reference is lvalue. Otherwise else, it is an rvalue reference.

The definition of make_tuple (with T deduced) now looks like this:

std::tuple<int&> make_tuple(int& && t){ 
    // ... 
}

Which collapses to:

std::tuple<int&> make_tuple(int& t){ 
    // ... 
}

So, back to your failed example:

std::tuple<int, int> fraction = make_tuple<int, int>( x, y );

Let's see what make_tuple looks like:

// since you explicitly called make_tuple<int,int>(), then no deduction occurs
std::tuple<int,int> make_tuple(int&& t1, int&& t2){ // Error! rvalue reference cannot bind to lvalue
    // ... 
}

Now it becomes clear why your example doesn't work. Since no reference collapsing occured, then T&& stayed int&&.

A revised version of your code should look something like this:

auto fraction = std::make_tuple(x,y);

I hope I explained this well.

Captain Hatteras
  • 490
  • 3
  • 11
  • Very good answer. But imho what is missing is showing how `std::tuple` is supposed to be used, i.e. without explicit template parameter. – bolov Nov 11 '21 at 18:47
  • My take on how `std::tuple` is supposed to be used is that it isn't. With structured binding and `std::make_tuple` + CTAD, the use-cases for a plain `std::tuple` has diminished greatly. Oh, and lambdas also helped diminish their prominence. – sweenish Nov 11 '21 at 19:24