Search code examples
c++rvostdmove

Will std::move() upon object construction in return statement help or prevent RVO?


Due to widely ranging responses from the community, I am asking this in hopes to debunk implementation-specific responses from stack-overflow users.

Which of these is best-practice (offers greatest optimization)?

// version 1
MyObject Widget::GetSomething() {
  return MyObject();
}

// version 2
MyObject Widget::GetSomething() {
  return std::move(MyObject());
}

// version 3
MyObject Widget::GetSomething() {
  auto obj = MyObject()
  return obj;
}

// version 4
MyObject Widget::GetSomething() {
  auto obj = MyObject()
  return std::move(obj);
}

EDIT: Thank you to Yakk, for the direct, respectful answer. [accepted answer]


Solution

  • // version 1
    MyObject Widget::GetSomething() {
      return MyObject();
    }
    

    In C++03 this requires MyObject by copyable. At runtime, no copy will be made using any "real" compiler with reasonable settings as the standard permits elision here.

    In C++11 or 14 it requires the object be movable or copyable. Elision remains; no move or copy is done.

    In C++17 there is no move or copy here to elide.

    In every case, in practice, MyObject is directly constructed in the return value.

    // version 2
    MyObject Widget::GetSomething() {
      return std::move(MyObject());
    }
    

    This is invalid in C++03.

    In C++11 and beyond, MyObject is moved into the return value. The move must occur at runtime (barring as-if elimination).

    // version 3
    MyObject Widget::GetSomething() {
      auto obj = MyObject();
      return obj;
    }
    

    Identical to version 1, except C++17 behaves like C++11/14. In addition, the elision here is more fragile; seemingly innocuous changes could force the compiler to actually move obj.

    Theoretically 2 moves are elided here in C++11/14/17 (and 2 copies in C++03). The first elision is safe, the second fragile.

    // version 4
    MyObject Widget::GetSomething() {
      auto obj = MyObject();
      return std::move(obj);
    }
    

    In practice this behaves just like version 2. An extra move (copy in C++03) occurs in constructing obj but it is elided, so nothing happens at runtime.

    Elision permits the elimination of side effects of the copy/move; the objects lifetimes are merged into one object, and the move/copy is eliminated. The constructor still has to exist, it is just never called.

    Answer

    Both 1 and 3 will compile to identical runtime code. 3 is slightly more fragile.

    Both 2 and 4 compile to identical runtime code. It should never be faster than 1/3, but if the move can be eliminated by the compiler proving not doing it is the same as-if doing it, it could compile to the same runtime code as 1/3. This is far from guaranteed, and extremely fragile.

    So 1>=3>=2>=4 is the order of faster to slower in practice, where "more fragile" code that is otherwise the same speed is <=.

    As an example of a case that could make 3 slower than 1, if you had an if statement:

    // version 3 - modified
    MyObject Widget::GetSomething() {
      auto obj = MyObject();
      if (err()) return MyObject("err");
      return obj;
    }
    

    suddenly many compilers will be forced to move obj into the return value instead of eliding obj and the return value together.