Does copy elision work with structured bindings
Asked Answered
M

2

26

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!

Mandelbaum answered 15/8, 2017 at 17:35 Comment(9)
Yes, and it's easy to see why when you consider what structured bindings actually desugar to. ;-]Trisect
@Trisect by yes are you confirming that one and two will not result in copy elision but three will?Mandelbaum
I mean 'yes, copy elision works with structured bindings' – one and three will result in guaranteed copy elision, two won't.Trisect
but those binding aren't in standard? and array needs extra argumentHavoc
@Trisect But one only allows access via get<> and tuple_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?Mandelbaum
@Mandelbaum : Again, it's important to understand what structured bindings actually desugar to. get<> and tuple_size are irrelevant.Trisect
@Trisect the way I understand it, first the decomposition is going to try and decompose the instance on the right manually without using (ADL defined or member) get<> or tuple_size and if that does not work. the implementation will fall back to using those. And in the case of std::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
@Trisect what did I get wrong in my understanding above?Mandelbaum
@Mandelbaum : You left out the "uniquely-named variable to hold the value of the initializer".Trisect
E
22

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

Eyelet answered 15/8, 2017 at 18:1 Comment(23)
wandbox.org/permlink/1pRlbWw06mVDguPN This is not how I have understood this. The way I understand it, structured bindings create an anonymous class/struct with the types of the variables being the same as those of the RHS (if tuple_element exists then that will be used to determine type) and then the regular decomposition process follows for that structMandelbaum
@Curious: Structured binding does nothing of the kind. It does not create classes at all.Targe
@NicolBolas I know it doesn't, that's just my mental model. And it has worked so far...Mandelbaum
Nitpick: names with double underscores (__tmp) are reserved - don't use such names in examples.Puppetry
@Mandelbaum make_tuple (and tuple in general) requires materializing temporaries to bind to the reference parameters of make_tuple (or tuple's constructor) when you create a tuple. That has nothing to do with structured bindings.Expander
@JesperJuhl I use names with double underscores in examples to illustrate names that are introduced by the language, much the same way the standard uses __range, __begin, and __end to define the meaning of a range-based for statement.Eyelet
@JesperJuhl It's intended to illustrate how the compiler desugars a structured binding declaration. And the compiler had better use reserved names for the variables it introduces!Expander
@Expander ah that makes sense. yes. I forgot to consider how the tuple constructor requires moving since it accepts forwarding referencesMandelbaum
@Eyelet could you mention how the decltype of a and b will not be what your syntactic sugar expansion has produced? They will not be references, but in your example they will. I will accept the answer then!Mandelbaum
@Expander My mental model was referring to your answer here https://mcmap.net/q/535093/-structured-bindings-when-something-looks-like-a-reference-and-behaves-similarly-to-a-reference-but-it-39-s-not-a-referenceMandelbaum
@Mandelbaum Doesn't the link to T.C.'s answer you just posted ^^ answer your question?Eyelet
@Eyelet I don't think so, where does it mention how temporaries fit in here?Mandelbaum
@Mandelbaum You're asking what decltype(a) means when a comes from a structured bindings. That is the topic of the linked question.Eyelet
@Eyelet I understand that, I meant the question I have asked above (the main question i.e.) about copy elision, not the one asking you to include that the syntax sugar above does not translate well to the decltype of the variables in the structured binding declarationMandelbaum
@Mandelbaum That's the first sentence of the answer: "Yes, all of them"Eyelet
@Eyelet Sorry I am not being clear. What I mean is, with your answer above it might make someone seem like the variables introduced by the structured bindings are of reference type (because you introduced them in the expansion with auto&), could you just mention that they are not references (may be, based on the types of the thing on the RHS) in your answer?Mandelbaum
@Mandelbaum They are references.Eyelet
@Eyelet but if you take a look at the cppreference documentation it says "In these initializer expressions, e is an lvalue if the type of the entity e is an lvalue reference (this only happens if the ref-operator is & or if it is && and the initializer expression is an lvalue) and an xvalue otherwise (this effectively performs a kind of perfect forwarding)" Meaning that the hidden variable is better described as being a forwarding reference?Mandelbaum
@Mandelbaum : e is a variable, formally; references are not variables. The part you're quoting regards the expression e and its value category in subsequent text. I mean you are conflating value category with storage; "a structured binding declaration first introduces a uniquely-named variable" is not ambiguous, and means a new object, not reference. If that doesn't sufficiently explain things then you need to ask a different question, TBH, or at least read the pages that have been linked here already.Trisect
@Trisect Doesn't type of the entity e is an lvalue reference imply that decltype(e) is an lvalue reference?Mandelbaum
You said you read that answer of @T.C.'s that you linked to; your questions strongly indicate otherwise. ;-]Trisect
@Trisect what did I miss?? The answer even says "even though the structured binding itself is in fact always a reference in this case", which does not say anything that would mean that the structured binding is not a reference type.Mandelbaum
The structured binding is not its uniquely-named variable; the former has the latter, but they are not the same semantically. The uniquely-named variable is always an object, because it is a 'variable' (in formal terms); the structured binding that sits atop that object represents itself differently (so as to appear to have the same ref-qualifier as the user's binding declaration). But again, do not conflate the two.Trisect
H
4

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.

Havoc answered 15/8, 2017 at 18:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.