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

1

15

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?

Hadrian answered 5/5, 2024 at 13:42 Comment(7)
This ctor is introduced by P2321.Ashy
@康桓瑋 I see that the const appears in P2321 as well. Do you see the reason in it ?Spectral
Everything is explained in P2214R0: A Plan for C++23 Ranges. @Barry mind answering this one?Extramural
@wohlstad, this constructor does exist in Microsoft STL: github.com/microsoft/STL/blob/…Ical
@Ical you are right, missed that.Spectral
@Spectral universal reference wouldn't work here - tuple<UTypes...> isn't template paramenter.Kitti
@Swift-FridayPie I see. I will delete my original comment.Spectral
R
4

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.

Ragen answered 2/6, 2024 at 11:42 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.