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.