Why do structured bindings depend on tuple_element?

  • A+

The most recent draft of the structured bindings proposal (on which the C++17 feature was based) requires std::tuple_size, member get or std::get, and std::tuple_element. Previous drafts require only std::tuple_size and member get or std::get. As far as I can tell, there was no discussion on adding this, it just appeared in the final draft. Is there a compelling reason to require the tuple_element specialization, considering I believe it can be implemented in general as

template<std::size_t index, typename T> struct tuple_element {     using type = decltype(std::get<index>(std::declval<T>())); }; 

Does anyone know of why this requirement was added?

Consider the case:

std::tuple<int, int&>& foo(); auto& [x, y] = foo(); 

What is decltype(x) and what is decltype(y)? The goal of the language feature is that x just be another name for foo().__0 and y be another name for foo().__1, which means that they should to be int and int&, respectively. As specificied today, this unpacks into:

auto& __e = foo(); std::tuple_element_t<0, decltype(__e)>& x = std::get<0>(__e); std::tuple_element_t<1, decltype(__e)>& y = std::get<1>(__e); 

And the rules work such that decltype(x) is the type to which x refers, so int. And decltype(y) is the type to which y refers, so int&.

If we avoided tuple_element, by doing something like:

auto&& x = std::get<0>(__e); auto&& y = std::get<1>(__e); 

Then we couldn't differentiate between x and y, because there is no way to differentiate between what std::get<0>(__e) and std::get<1>(__e) do: both give back an int&.

This is also the way to add consistency between the above case and the normal struct case:

struct C {     int i;     int& r; }; C& bar(); auto& [a, b] = bar(); 

We want, for the purposes of structured bindings, for a and b here to behave the same way as x and y there. And a and b here aren't introduced variables, they're just different names for __e.i and __e.r.

In the non-reference case, there is a different scenario where we cannot differentiate:

std::tuple<int, int&&> foo(); auto [x, y] = foo(); 

Here, we at present unpack via:

auto __e = foo(); std::tuple_element_t<0, decltype(e)>& x = std::get<0>(std::move(__e)); std::tuple_element_t<1, decltype(e)>& y = std::get<1>(std::move(__e)); 

Both std::get calls return an int&&, so you couldn't differentiate between them using auto&&... but the results of tuple_element_t are different - int and int&&, respectively. This difference could be seen with the normal struct case too.

Note that due to CWG 2313, actually the unpacking happens into a uniquely named variable reference and the identifiers specified into the binding just refer to those objects.


:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: