3

In term of move semantics on return value, I heard that the best practice is to return the object as is, and don't call std::move to it, as it prevent return value optimizations. However, consider the following code.

class MyTestClass {
public:
    // intentionally implicit constructor
    MyTestClass(const std::string& s) {
        std::cerr << "construct " << s << std::endl;
    }

    MyTestClass(std::string&& s) {
        std::cerr << "move construct " << s << std::endl;
    }

    MyTestClass(int s) {
        std::cerr << "construct int " << s << std::endl;
    }
};

void testRVO() {
    auto lambda = [](int t) -> MyTestClass {
        if (t == 0) {
            std::string s("hello");
            return s;  // use move, RVO
        } else if (t == 1) {
            std::optional<std::string> s("hello");
            // or unique pointer
            // std::unique_ptr<std::string> s = std::make_unique<std::string>("hello");
            return *s;  // use copy, no RVO !!!
        } else if (t == 2) {
            std::optional<std::string> s("hello");
            return std::move(*s);  // use move
        } else {
            return 1;
        }
    };

    lambda(0);
    lambda(1);
    lambda(2);

    lambda(-1);
}

MyTestClass simulates a generic wrapper/container (think of std::variant) that can be implicitly constructed from const T& and T&&.

In the first case, I returned a string as is, and move constructor is called, which is what you would expect.

In the second case, I constructed an std::optional (or std::unique_ptr) and returns its value using the dereference operator. However, in this case, copy constructor is called instead.

In the third case, similar to previous case, I explicitly called std::move in the return statement and the move constructor is called.

I am not sure why the copy constructor is called in the second case, as the dereference operator returns a lvalue reference, which is an lvalue. The string s in first case is also an lvalue. So I would expect them to be treated the same. Is the compiler just not smart enough to tell that the std::optional would be destroyed after return anyway? Or is it because I am using implicit construction?

In practice, I would obviously want to use the third case, since copying s can be much more expensive than moving it (consider s is a larger, more complex object), but it still feels wrong to write return std::move(...);

Could someone explain why this happens? When should I use std::move in return statement?

6
  • return s; // use move, RVO - the comment is confusing. s is not MyTestClass and is copied and not moved. Commented Jun 12, 2024 at 19:37
  • 1
    return *s; // use copy, no RVO !!! is also confusing. RVO is applied to MyTestClass. Commented Jun 12, 2024 at 19:39
  • 2
    std::optional is not available is C++11, why do you tag the question so? Commented Jun 12, 2024 at 19:41
  • 3
    "In term of move semantics on return value, I heard that the best practice is to return the object as is, and don't call std::move to it" - In return *s; you aren't returning "the object as is", you are returning an expression *s. Commented Jun 12, 2024 at 19:44
  • 2
    Other scenario to consider - copy elision, where neither copy or move semantics are used, but rather the caller's receiving variable is initialized directly with the value being return'ed. Commented Jun 12, 2024 at 19:55

2 Answers 2

8

With:

std::string s("hello");
return s;  // use implicit move

s is move-eligible.

Before C++23, since this selects a constructor whose first parameter has type std::string&&, this is "implicitly" moved from.
After C++23, all move-eligible expressions are implicitly moved from (P2266R3).


The reason this isn't done for std::optional<std::string> s; return *s; is that you don't return s directly, but the result of *s. Imagine s was std::reference_wrapper<std::string> s = global_string_I_dont_want_to_move_from;, the compiler doesn't know that *s is "local" to the function.

*s in this case is s.operator*(). Like any function f(s), the compiler has no idea if it is allowed to move from the result of the function call.

The only move-eligible things are local variables that are definitely going to go out of scope after the return.


In general, if a return statement could undergo NRVO (i.e., the type of the variable is the same as the return type), do not use std::move, as it might pessimize an elision into a move.

In C++23, if you return a move-eligible expression, you don't have to std::move.
Before C++23, if you return a move-eligible expression that has a different type than the return type, use std::move to be predictable.

If the expression isn't move-eligible then use std::move if you want to move from it, the same as any other initialization.

Sign up to request clarification or add additional context in comments.

Comments

0

the best practice is to return the object as is, and don't call std::move to it, as it prevent[s] return value optimizations.

This is correct, but you do not return the object "as is" for any t. Instead, you construct MyTestClass from the value you put in return statement. For example, if you wrote

return std::move(s);

then you still would not prevent RVO for MyTestClass object. Instead, you would construct MyTestClass with std::string&& argument. So, in case of returning an implicitly constructed "generic wrapper", using std::move() in return statement is always safe and optimal.

2 Comments

I see. But I think that's only part of the story, even if I explicitly constructed std::optional<MyClass> object, return *obj; uses the copy constructor of MyClass whereas return std::move(*obj); uses the move constructor. So apparently the compiler is dumb, and the action of dereferencing means explicit std::move is needed
You don't have user defined copy or move constructors. What you call so all three are converting constructors.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.