6

When returning an object from a function one of the following cases can happen since C++11, assuming move- and copy-constructors are defined (see also the examples at the end of this post):

  1. it qualifies for copy-elision and compiler performs RVO.
  2. it qualifies for copy-elision and compiler doesn't perform RVO, but then ...
  3. it qualifies for the usage of move constructor and is moved.
  4. none of the above and the copy-constructor is used.

The advice for the first 3 cases is not to use the explicit std::move, because move is performed anyway and could prevent possible RVO, see for example this SO-post.

However, in the 4. case an explicit std::move would improve the performance. But as somebody who is fluent in reading neither the standard nor the resulting assembler, it takes a lot of time to differentiate between cases 1-3 and 4.

Thus my question: Is there are a way to handle all of the above cases in an unified manner, such that:

  1. RVO is not impeded (case 1)
  2. if RVO is not performed, move-constructor is used (cases 2,3 and 4)
  3. if there is no move-constructor, copy-constructor should be used as fallback.

Here are some examples, which also can be used as test cases.

All examples use the following helper-class-definition:

struct A{
    int x;
    A(int x_);
    A(const A& a);
    A(A&& a);
    ~A();
};

1. example: 1.case, RVO performed, live-demonstration, resulting assembler:

A callee1(){
    A a(0);
    return a;
}

2. example: 1.case, RVO performed, live-demonstration, resulting assembler:

A callee2(bool which){
    return which? A(0) : A(1);
}

3. example: 2.case, qualifies for copy-elision, RVO not performed, live-demonstration, resulting assembler:

A callee3(bool which){
    A a(0);
    A b(1);
    if(which)
      return a;
    else
      return b; 
}

4. example: 3.case, doesn't qualify for copy-elision (x is a function-parameter), but for moving, live-demonstration, resulting assembler:

A callee4(A x){
    return x; 
}

5. example: 4.case, no copy-elision or implicit moving (see this SO-post), live-demonstration, resulting assembler:

A callee5(bool which){
    A a(0);
    A b(1);
    return which ? a : b;
}

6. example: 4.case, no copy-elision or implicit moving, live-demonstration, resulting assembler:

A callee6(){
    std::pair<A,int> x{0,1};
    return x.first;
}
ead
  • 32,758
  • 6
  • 90
  • 153
  • 4
    Not really an answer but gcc and clang have started to warn on pessimizing moves: https://godbolt.org/z/ZkpDgB – Rakete1111 Apr 29 '19 at 09:04

1 Answers1

3

When can't RVO be performed?

The compiler (usually) can't perform RVO if any of the following apply:

  1. which local variable gets returned depends on a conditional (with local variables being defined before the conditional, rather than inside it)
  2. you're returning a member of a class, union, or struct
  3. you're dereferencing a pointer to get the return value (this includes array indexing)

In case 1, you should either use std::move or write the code using an if statement instead of the ternary operator. In case 2, you have to use std::move. In case 3, you should also use std::move explicitly.

Policy for ensuring move construction (if possible)

Handling case 1. We can ensure the value being moved by relying on if statements for returning local variables, instead of the ternary operator:

A whichOne(bool which) {
    A a(0); 
    A b(1); 
    if(which) {
        return a;
    } else { 
        return b; 
    }
}

If you really prefer using the ternary operator, use std::move explicitly on both conditionals:

A whichOne(bool which) {
    A a(0); 
    A b(1); 
    return which ? std::move(a) : std::move(b); 
}

What if I only want to move a, but not b? We can handle this case by explicitly constructing it in the conditional. In this case, the constructed value is guaranteed to undergo RVO, since both sides of the ternary produce a prvalue.

A whichOne(bool which) {
    A a(0);
    A b(1); 
    return which 
      ? A(std::move(a)) // prvalue move-constructed from a
      : A(b);           // prvalue copy-constructed from b
}

Handling case 2 and 3. This is pretty straight-forward: use std::move when returning a member of an object, or when returning a reference that came from an array or pointer.

Alecto Irene Perez
  • 10,321
  • 23
  • 46
  • My question is not about these special examples - it is more about an unified manner, so one doesn't have to know all the details. – ead Apr 29 '19 at 09:44
  • I guess my message is “only annotate with move if you really, really have to”? The cases I discussed are edge cases precisely because the language can’t judge the programmers intent. You can have a good coding standard so that move always gets used (that’s what my answer tries to provide), but precisely because of the ambiguity inherent in these cases you have to treat them differently than the ones where moves or RVO occur automatically. – Alecto Irene Perez Apr 29 '19 at 09:49
  • That being said, the exceptions to the rule are both relatively uncommon, and it’s clear when they occur. Returning an element of an array or a member of a class *looks different* than just returning a local variable. Returning the result of a ternary *looks different* than just returning a value. You don’t have to know the details of the rest of the function to apply my rule; you only have to look at the return statement – Alecto Irene Perez Apr 29 '19 at 09:51
  • "_member of a class, union, or struct_" technically all are called "class". See the [class-key](http://eel.is/c++draft/class#nt:class-key) in the grammar. – curiousguy Apr 29 '19 at 20:42