25

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!

Curious
  • 20,870
  • 8
  • 61
  • 146
  • Yes, and it's easy to see why when you consider what structured bindings actually desugar to. ;-] – ildjarn Aug 15 '17 at 17:44
  • @ildjarn by yes are you confirming that `one` and `two` will not result in copy elision but `three` will? – Curious Aug 15 '17 at 17:45
  • 1
    I mean 'yes, copy elision works with structured bindings' – `one` and `three` will result in guaranteed copy elision, `two` won't. – ildjarn Aug 15 '17 at 17:47
  • but those binding aren't in standard? and array needs extra argument – Swift - Friday Pie Aug 15 '17 at 17:48
  • @ildjarn 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? – Curious Aug 15 '17 at 17:49
  • @Curious : Again, it's important to understand what structured bindings actually desugar to. `get<>` and `tuple_size` are irrelevant. – ildjarn Aug 15 '17 at 17:50
  • @ildjarn 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) – Curious Aug 15 '17 at 17:52
  • @ildjarn what did I get wrong in my understanding above? – Curious Aug 15 '17 at 17:53
  • @Curious : You left out the "*[uniquely-named variable to hold the value of the initializer](http://en.cppreference.com/w/cpp/language/structured_binding)*". – ildjarn Aug 15 '17 at 17:57

2 Answers2

21

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

G. Sliepen
  • 7,637
  • 1
  • 15
  • 31
Barry
  • 286,269
  • 29
  • 621
  • 977
  • https://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 struct – Curious Aug 15 '17 at 18:04
  • 2
    @Curious: Structured binding does nothing of the kind. It does not create classes at all. – Nicol Bolas Aug 15 '17 at 18:10
  • @NicolBolas I know it doesn't, that's just my mental model. And it has worked so far... – Curious Aug 15 '17 at 18:11
  • 1
    Nitpick: names with double underscores (`__tmp`) are reserved - don't use such names in examples. – Jesper Juhl Aug 15 '17 at 18:13
  • 2
    @Curious `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. – T.C. Aug 15 '17 at 18:15
  • 9
    @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. – Barry Aug 15 '17 at 18:16
  • 7
    @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! – T.C. Aug 15 '17 at 18:16
  • @T.C. ah that makes sense. yes. I forgot to consider how the tuple constructor requires moving since it accepts forwarding references – Curious Aug 15 '17 at 18:17
  • @Barry 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! – Curious Aug 15 '17 at 18:23
  • @T.C. My mental model was referring to your answer here https://stackoverflow.com/a/44696826/5501675 – Curious Aug 15 '17 at 18:25
  • @Curious Doesn't the link to T.C.'s answer you just posted ^^ answer your question? – Barry Aug 15 '17 at 18:26
  • @Barry I don't think so, where does it mention how temporaries fit in here? – Curious Aug 15 '17 at 18:27
  • @Curious You're asking what `decltype(a)` means when `a` comes from a structured bindings. That is the topic of the linked question. – Barry Aug 15 '17 at 18:30
  • @Barry 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 declaration – Curious Aug 15 '17 at 18:31
  • @Curious That's the first sentence of the answer: "Yes, all of them" – Barry Aug 15 '17 at 18:43
  • @Barry 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? – Curious Aug 15 '17 at 18:45
  • @Curious They *are* references. – Barry Aug 15 '17 at 18:46
  • @Barry 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? – Curious Aug 15 '17 at 22:01
  • 1
    @Curious : `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. – ildjarn Aug 15 '17 at 22:04
  • @ildjarn Doesn't `type of the entity e is an lvalue reference` imply that `decltype(e)` is an lvalue reference? – Curious Aug 15 '17 at 22:10
  • 1
    You said you read that answer of @T.C.'s that you linked to; your questions strongly indicate otherwise. ;-] – ildjarn Aug 15 '17 at 22:12
  • @ildjarn 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. – Curious Aug 15 '17 at 22:17
  • 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. – ildjarn Aug 15 '17 at 22:32
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.

Swift - Friday Pie
  • 12,777
  • 2
  • 19
  • 42