26

Suppose we have a class named AAA that supports both copy/move:

class AAA
{
public:
    AAA() = default;
    ~AAA() = default;

    AAA(const AAA& rhs)
    {
       std::cout << "Copy constructor" << std::endl;
    }

    AAA(AAA&& rhs)
    {
       std::cout << "Move constructor" << std::endl;
    }
};

In the following code, get_val returns second:

AAA get_val()
{
    auto [ first, second ]  = std::make_tuple(AAA{}, AAA{});

    std::cout << "Returning - " << std::endl;
    return second;
}

auto obj = get_val();
std::cout << "Returned - " << std::endl;

Now second is copied, printing the following output:

...
Returning - 
Copy constructor 
Returned -

This is unfortunate, because my expectation of the result is either there's no call to copy constructor, or at least it's implicitly moved.

To avoid copying, I'll have to explicitly apply std::move on it.

return std::move(second);

Then I'd receive the result of:

...
Returning - 
Move constructor 
Returned - 

I assume that the reason RVO is not performed is that probably the compilers would see second as a reference instead, while get_val returns prvalue.

However why can implicit move NOT be expected either? Using explicit std::move on the return statement does not look intuitive in this particular case, because you generally don't want to make RVO, which is in most cases a better optimization than move, accidentally gone away.

Tested by both compilers gcc and clang with -O3.

Live Demo

Dean Seo
  • 5,486
  • 3
  • 30
  • 49
  • 1
    NRVO is clearly impossible: that requires that the object be constructed in the calling context, but the actual construction is a `tuple`, not an AAA. The names `first, second` refer to the elements of the tuple. Currently the "move context" is a subset of NRVO, but I don't see any reason why there couldn't be a move context added for members of non-reference structured bindings . However it is safe to manually write `std::move(second)` , since there is no NRVO then this can't possibly be a case of "pessimizing move" – M.M Apr 01 '18 at 08:30
  • @M.M Right. I get that part that rvo can't be performed, and the manually applying `std::move` makes it safe. But why though? It'd be nice to have the compiler make sure of that for us. I'm wondering there's any quote in the standard that prevents the compilers from applying implicit `std::move`. – Dean Seo Apr 01 '18 at 08:32
  • The standard says that this "implicit move" only applies to "an id-expression that names an object with automatic storage duration" (see [class.copy.elision]/3.1). Which doesn't apply to the structured binding , those names name the result of `std::get` . I interpreted your question to be asking about the rationale behind this – M.M Apr 01 '18 at 08:35
  • @M.M I don't see why your quote prevents elision. The last sentence in [\[dcl.struct.bind\]/3](https://timsong-cpp.github.io/cppwp/dcl.struct.bind#3) is very confusing. It seems here the `second` should be a copy in itself, and thus "automatic storage duration" is satisfied. – llllllllll Apr 01 '18 at 09:09
  • @liliscent `second` directly refers to the element of the tuple, there's no copy. Not sure how you get that from the quote you indicate, which explicilty says that the name (v_i) refers to the object bound to the "hidden" references r_i. (Those references are bound directly to the elements of the tuple). I can see that there might be wiggle room for someone to argue that the names refer to a subobject of `e`, and `e` is an object of automatic storage duration therefore its subobjects are; but I don't think the definition was intended that way . (But I could be wrong, this is all speculation) – M.M Apr 01 '18 at 09:16
  • @M.M All in the tuple get moved with the binding and the elision fails to engage, so we got a copy right [**there**](https://wandbox.org/permlink/1VjSnSutZmPCnJBb) <= (sorry, I just follow this from the old post, so the code is in original post) – sandthorn Apr 01 '18 at 09:24
  • @M.M 'bound to the "hidden" references' is exactly what I meant "confusing". I don't recall other place in standard where this concept is mentioned. And the final result is that the `second` is standalone, and you can't modify the original tuple by modifying this `second`. – llllllllll Apr 01 '18 at 09:28
  • Honestly, I can't see any issues if compiler would do copy elision on a structured binded lvalue because it does **exactly that** on `std::get`ed lvalues => [**here**](https://wandbox.org/permlink/fdhk5flWLah6JNom). A bug in both compilers perhaps? – sandthorn Apr 01 '18 at 11:01
  • @liliscent Yes it does modify the original tuple . – M.M Apr 01 '18 at 11:59
  • @sandthorn in your second example `auto second = ANYTHING; .... return second;` means that `second` is a copy elision context . You seem to show a bunch of different examples about how it was initialized but that is irrelevant. Not sure what you are pointing out exactly. – M.M Apr 01 '18 at 12:00
  • @M.M [This is definitely wrong.](https://wandbox.org/permlink/Aj5g7FxJ2Vg771LK) – llllllllll Apr 01 '18 at 12:04
  • 1
    @liliscent in `auto [b] = e;`, the bindings are bound to a copy of `e` . You seem mixed up, that `e` is not the same `e` mentioned by the standard (the hidden object that bindings are bound to). That is nothing to do with the code in this question. , in which a prvalue is bound to – M.M Apr 01 '18 at 12:08
  • @M.M I forgot to read the first section. You're right... I'm wrong... Thanks. – llllllllll Apr 01 '18 at 13:49

1 Answers1

20

However why can implicit move NOT be expected either?

For the exact same reason as elision is turned off: because it's a reference, not the name of an independent object. Every use of second is essentially equivalent to saying obj.whatever or get<1>(obj) (though in the latter case, we store the reference). And there's no implicit move from either of those expressions.

Structured binding is for accessing subobjects of the given object. You cannot elide returns of subobjects, nor can you implicitly move from them. Therefore, you cannot elide structured binding names nor implicitly move from them.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 1
    **because it's a reference, not the name of an independent object.** Right, but it's *conceptually* a reference to the hidden object though. `second`'s external state's type is still `AAA`, non-reference type, if you do `decltype(second)`. I find it counter-intuitive if we have to keep reminding ourselves that its behind scene will always come with `std::get`? – Dean Seo Apr 01 '18 at 16:10
  • 1
    @DeanSeo: "*Right, but it's conceptually a reference to the hidden object though.*" Yeah, that's the point. It's *part of* some other object; it is not independent of the hidden object. Implicitly moving someone else's parts is rude. – Nicol Bolas Apr 01 '18 at 16:14
  • To me, *`obj.whatever` can't be implicitly moved* sounds more convincing than the `std::get`-although-a-reference-is-returned approach since in that way the two types are the same. Probably this Structured Binding feature must be the first time in C++ that the external type that we see and the internal type that compilers see are not equivalent. – Dean Seo Apr 01 '18 at 16:21
  • @DeanSeo: "*Probably this Structured Binding feature must be the first time in C++ that the external type that we see and the internal type that compilers see are not equivalent.*" In what way are they not equivalent? `obj.whatever` is an lvalue, as is `second`; neither are references. If it's a tuple-like type, then `std::get` returns a reference, and `second` will be a reference as well. So where is this "external type" and "internal type" that are distinct? – Nicol Bolas Apr 01 '18 at 17:10
  • *"In what way are they not equivalent?"* Right they're both lvalue but not the same type, because `decltype(second)` deduces `AAA` while `second` is treated by compiler as a reference, *a reference to the hidden object* which is `AAA&`. AFAIK this is how compilers actually translate Structured Bindings in fact. – Dean Seo Apr 02 '18 at 04:11
  • `second` is in the form of a local variable but it fails to meet the condition of both RVO and implicit move because it's actually not (to the compiler). That's (at least) how I understood this. – Dean Seo Apr 02 '18 at 04:17
  • 1
    @DeanSeo: "*while second is treated by compiler as a reference*" No, it's treated exactly like the compiler would treat `obj.whatever`. `decltype(obj.whatever)` would be `AAA`, but the compiler would not move from it. – Nicol Bolas Apr 02 '18 at 04:24
  • 2
    @DeanSeo: "*it fails to meet the condition of both RVO and implicit move because it's actually not (to the compiler)*" It fails to meet those requirements because it is not a variable that declares a complete object. It is either a subobject of another object (`obj.whatever`) or a reference to another object (`get<1>(obj)`), neither of which can be implicitly moved from. – Nicol Bolas Apr 02 '18 at 04:27
  • Understood. Thanks. – Dean Seo Apr 02 '18 at 04:27
  • A reference pretender that can bind to rvalue!? Really? This behaviour looks more like a bug if I may say. Okey, just pretend it's not, so what does this all give then? – sandthorn Apr 02 '18 at 11:12
  • 2
    @sandthorn: "*A reference pretender that can bind to rvalue!? Really?*" Nothing in this example is an rvalue, nor does any of it bind to an rvalue. So what are you talking about? "*so what does this all give then?*" Again, I'm not sure what you mean by that. What structured binding gives you is a way to access the member subobjects of an object without having to use opaque tools like `get<1>`, as well as applying a meaningful name to the values. It's pure syntactic sugar. – Nicol Bolas Apr 02 '18 at 14:58