c++c++17rvaluecopy-elisionstructured-bindings

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!


Solution

  • 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>& one = some_getter<0>(__tmp);
    some_type<1,E>& two = 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&&.