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?
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?