std::tuple
(or std::pair
) is one of the basic building blocks in the standard library, and we can use it to hold a tuple of values, references, or even a mix of them. That is, tuple
can behave like value types or reference types. If we only care about value tuples, your feeling is somewhat correct: the const tuple<Us...>&&
overload is basically meaningless. However, things get a bit complicated when we introduce reference tuples.
Just as int&
can bind to an lvalue of type int
, it is natural to assume that tuple<int&>
can be initialized from an lvalue of type tuple<int>
(i.e. tuple<int>&
). Furthermore, since references are baked into the type of tuple
, is it reasonable to initialize a tuple<int&>
from an lvalue of tuple<int&>
(i.e. tuple<int&>&
)? The answer is yes. Thanks to reference collapsing, get<0>
for tuple<int&>&
has a return type of int&
. The following table shows some examples:
tuple |
get<0> |
[const ] tuple<int&>& |
int& |
[const ] tuple<int&&>& |
int& |
[const ] tuple<int&>&& |
int& |
[const ] tuple<int&&>&& |
int&& |
Note that the top-level const
applied to tuple
doesn't affect the return type since references themselves are already const
-like (one reference can only bind to one object).
One motivating example of conversions between reference tuples and value tuples given in P2214R2 is views::zip
. zip
uses tuple
(or pair
) to wrap multiple ranges::range_value_t
(typically T
) and ranges::range_reference_t
(typically T&
) of the underlying views. For example, zip
of two std::vector<int>
has tuple<int, int>
as its range_value_t
and tuple<int&, int&>
as its range_reference_t
(note that tuple<int, int>&
is not suitable in this case because we have two views of int
instead of one view of tuple<int, int>
). We really want these two types to model T
and T&
. Thus, P2214R2 proposes to add the following overloads:
template<class... Us>
tuple(tuple<Us...>&); // (1)
template<class... Us>
tuple(const tuple<Us...>&&); // (2)
The overload (2) is added just for consistency and completeness.
But wait, why bother adding all these boilerplates when we could just utilize perfect forwarding? Indeed, P2165R4 proposes to add the following universal constructor:
template<tuple-like U>
tuple(U&&);
So can we get rid of all those redundant single-tuple
/pair
constructors? Probably not, for backward compatibility reasons. Let's just keep them since they seem "harmless" in all cases. Don't be surprised that tuple
now has 12 constructors (excluding copy and move constructors). However, there do exist cases where these redundant constructors are undesirable and can be harmful. Consider the following code (Godbolt):
using T = std::tuple<int &>;
using U = std::tuple<int &&>;
T t = U(42);
The above code calls the const tuple<int&&>&
overload and creates a dangling reference immediately. The tuple<int&&>&&
overload isn't viable in this case since int&&
can't bind to int&
. Then we fall back to const tuple<int&&>&
, which, as shown above, produces an int&
. Without these overloads, one universal constructor could properly reject the above code.
const
appears in P2321 as well. Do you see the reason in it ? – Spectraltuple<UTypes...>
isn't template paramenter. – Kitti