Interesting question:
#include <iostream>
#include <array>
#include <tuple>
#include <typeinfo>
using std::cout;
using std::endl;
struct SomeClass
{
int baz;
SomeClass(int _b): baz(_b) {
cout << __PRETTY_FUNCTION__ << " = " << baz << endl;
}
SomeClass(SomeClass&&) {
cout << __PRETTY_FUNCTION__ << endl;
}
SomeClass(const SomeClass&) {
cout << __PRETTY_FUNCTION__ << endl;
}
};
template<typename T> void tell(T&& a)
{
cout << "Tell: " << __PRETTY_FUNCTION__ << " = " << a.baz << endl;
}
int main()
{
// one
cout << "= 1 =" << endl;
auto [one, two] = std::array<SomeClass,2>{SomeClass{1}, SomeClass{2}};
cout << "===" << endl;
tell(one); tell(two);
// two
cout << endl << "= 2 =" << endl;
auto [one2, two2] = std::make_tuple(SomeClass{1}, SomeClass{2});
cout << "===" << endl;
tell(one2); tell(two2);
// three
cout << endl << "= 3 =" << endl;
struct Something { SomeClass one{1}, two{2}; };
auto [one3, two3] = Something{};
cout << "===" << endl;
tell(one3); tell(two3);
return 0;
}
Produces output:
= 1 =
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(int) = 2
===
Tell: void tell(T&&) [with T = SomeClass&] = 1
Tell: void tell(T&&) [with T = SomeClass&] = 2
= 2 =
SomeClass::SomeClass(int) = 2
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(SomeClass&&)
SomeClass::SomeClass(SomeClass&&)
===
Tell: void tell(T&&) [with T = SomeClass&] = 0
Tell: void tell(T&&) [with T = SomeClass&] = 4199261
= 3 =
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(int) = 2
===
Tell: void tell(T&&) [with T = SomeClass&] = 1
Tell: void tell(T&&) [with T = SomeClass&] = 2
Second case uses either copy or move (if available) constructor. Values weren't initialized, because I intentionally didn't do that in constructors.
There are three protocols of binding
- binding to array
- binding to tuple-like type
- binding to public data members
In second case (sorry, I don't have access to C++17 pdf, so cppreference):
Each identifier becomes a variable whose type is "reference to
std::tuple_element<i, E>::type
": lvalue reference if its corresponding
initializer is an lvalue, rvalue reference otherwise. The initializer
for the i-th identifier is
e.get<i>()
, if lookup for the identifier get in the scope of E by class member access lookup finds at least one declaration (of whatever
kind)
- Otherwise,
get<i>(e)
, where get is looked up by argument-dependent lookup only, ignoring non-ADL lookup
First and second stage of example are actually bindings to tuple-like type.
But... In second stage what we use to initialize? A template function that constructs tuple:
std::make_tuple(SomeClass{1}, SomeClass{2});
which would actually either copy or move values. Further copy elision may occur, but
auto t = std::make_tuple(SomeClass{1}, SomeClass{2});
auto [one2, two2] = t;
would produce this output:
SomeClass::SomeClass(int) = 2
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(SomeClass&&) //make_tuple
SomeClass::SomeClass(SomeClass&&)
SomeClass::SomeClass(const SomeClass&) //assignment
SomeClass::SomeClass(const SomeClass&)
Although properly de-sugaring structured binding looks like:
auto t = std::make_tuple(SomeClass{1}, SomeClass{2});
auto& one2 = std::get<0>(t);
auto& two2 = std::get<1>(t);
and output matches original:
SomeClass::SomeClass(int) = 2
SomeClass::SomeClass(int) = 1
SomeClass::SomeClass(SomeClass&&)
SomeClass::SomeClass(SomeClass&&)
===
So, the copy or move operation that happens, is from constructing our tuple
.
We would avoid that, if we construct tuple using universal references, then both desugared
auto t = std::tuple<SomeClass&&, SomeClass&&>(SomeClass{1}, SomeClass{2});
auto& one2 = std::get<0>(t);
auto& two2 = std::get<1>(t);
and structured binding
auto [one2, two2] = std::tuple<SomeClass&&, SomeClass&&>(SomeClass{1}, SomeClass{2});
would result in copy elision.
one
andtwo
will not result in copy elision butthree
will? – Mandelbaumone
andthree
will result in guaranteed copy elision,two
won't. – Trisectget<>
andtuple_size
function/method/traits? From what it seemed like the thing on the right hand side (from what I could make out by reading the standard) has to be either an array or a class that has all public members, can structured bindings be recursive in this way? – Mandelbaumget<>
andtuple_size
are irrelevant. – Trisectget<>
ortuple_size
and if that does not work. the implementation will fall back to using those. And in the case ofstd::array
I'm not sure if the decomposition falls back recursively to the contained array member. Despite the thing being an aggregate (aggregates were not mentioned in the standard spec for structured bindings if I recall correctly) – Mandelbaum