Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does copy elision work with structured bindings

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!

like image 725
Curious Avatar asked Aug 15 '17 17:08

Curious


People also ask

Is copy elision guaranteed?

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.

What is a structured binding?

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.

What is a guaranteed copy?

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();}


2 Answers

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&&.

like image 55
Barry Avatar answered Sep 19 '22 21:09

Barry


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.

like image 31
Swift - Friday Pie Avatar answered Sep 21 '22 21:09

Swift - Friday Pie