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?

8
  • 4
    T&& in a template is not an rvalue reference, it is a forwarding reference, which can bind to lvalues and rvalues. Commented Nov 11, 2021 at 17:36
  • 6
    You'd need to use std::make_tuple<int&, int&>( x, y ); because make_tuple uses forwarding references. But in practice you would just use std::make_tuple( x, y ); and let template argument deduction do the work for you. Commented Nov 11, 2021 at 17:36
  • 5
    Explicitly specifying the parameters of make_tuple loses its original purpose. Commented Nov 11, 2021 at 17:38
  • @康桓瑋 How so? What was its original purpose? Commented Nov 11, 2021 at 17:52
  • 3
    @bobobobo You can create a tuple with explicit types with tuple<A,B>(a,b), no need for make_tuple. The whole point of make_tuple is to let the compiler deduce the types. (later came some changes so we can use plain tuple like make_tuple) Commented Nov 11, 2021 at 18:38

1 Answer 1

8

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Very good answer. But imho what is missing is showing how std::tuple is supposed to be used, i.e. without explicit template parameter.
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.
Do the same rules apply when calling the tuple constructor directly? It looks like calling the tuple constructor in the same way with explicitly specified types compiles just fine.
@user21796467 No, std::tuple<Types...> has a constructor which takes const Types&.... Also see see this question.
@user21796467 sorry for the typo haha. I hope I've answered your question?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.