Does mandatory copy elision apply to decomposition via structured bindings? Which of the following cases does that apply to?
// one
auto [one, two] = std::array<SomeClass>{SomeClass{1}, SomeClass{2}};
// two
auto [one, two] = std::make_tuple(SomeClass{1}, SomeClass{2});
// three
struct Something { SomeClass one, two; };
auto [one, two] = Something{};
I suspect only the third case allows for copy elision, since the first two will be "decomposed" via std::get<>
and std::tuple_size<>
and std::get<>
returns xvalues when the arguments are rvalues
A quote from the standard would be nice also!
In C++20, the only copy allowed in the example is the one at line 3 (actually, x is implicitly moved from). Copy elision (NRVO) is allowed there and is routinely performed by most compilers, but is still non-guaranteed, and the widget class cannot be non-copyable non-movable.
A structured binding declaration introduces all identifiers in the identifier-list as names in the surrounding scope and binds them to subobjects or elements of the object denoted by expression. The bindings so introduced are called structured bindings.
Guaranteed copy elision redefines a number of C++ concepts, such that certain circumstances where copies/moves could be elided don't actually provoke a copy/move at all. The compiler isn't eliding a copy; the standard says that no such copying could ever happen. Consider this function: T Func() {return T();}
Does mandatory copy elision apply to decomposition via structured bindings? Which of the following cases does that apply to?
Yes, all of them. The point of structured bindings is to give you named references to the destructured elements of the type you're binding to. This:
auto [one, two] = expr;
Is just syntax sugar for:
auto __tmp = expr;
some_type<0,E>& a = some_getter<0>(__tmp);
some_type<1,E>& b = some_getter<1>(__tmp);
Where some_type
and some_getter
depend on the kind of type we're destructuring (array, tuple-like, or type with all public non-static data members).
Mandatory copy elision applies in the auto __tmp = expr
line, none of the other lines involve copies.
There's some confusion around an example in the comments, so let me elaborate on what happens in:
auto [one, two] = std::make_tuple(Something{}, Something{});
That expands into:
auto __tmp = std::make_tuple(Something{}, Something{}); // note that it is from
// std::make_tuple() itself that we get the two default constructor calls as well
// as the two copies.
using __E = std::remove_reference_t<decltype(__tmp)>; // std::tuple<Something, Something>
Then, since __E
is not an array type but is tuple-like, we introduce variables via an unqualified call to get
looked up in the associated namespace of __E
. The initializer will be an xvalue and the types will be rvalue references:
std::tuple_element_t<0, __E>&& one = get<0>(std::move(__tmp));
std::tuple_element_t<1, __E>&& two = get<1>(std::move(__tmp));
Note that while one
and two
are both rvalue references into __tmp
, decltype(one)
and decltype(two)
will both yield Something
and not Something&&
.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With