Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to ensure moving without impeding RVO?

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;
}
like image 869
ead Avatar asked Apr 29 '19 08:04

ead


1 Answers

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.

like image 72
Alecto Irene Perez Avatar answered Sep 20 '22 01:09

Alecto Irene Perez