Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does std::tuple have a const tuple<UTypes...>&& constructor?

When examining the constructors of std::tuple on the cppreference site, I came across the move constructor defined below, which is available in C++23.

template< class... UTypes >
constexpr tuple( const tuple<UTypes...>&& other )

Here, constexpr indicates that the constructor can be processed at compile time if possible. However, doesn't the parameter being const make the move semantics meaningless, or did I completely misunderstand?

like image 627
Suat Mutlu Avatar asked Feb 14 '26 00:02

Suat Mutlu


1 Answers

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.


Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!