Is it possible to write C++ code where we rely on the return value optimization (RVO) when possible, but fall back on move semantics when not? For example, the following code can not use the RVO due to the conditional, so it copies the result back:
#include <iostream>
struct Foo {
Foo() {
std::cout << "constructor" << std::endl;
}
Foo(Foo && x) {
std::cout << "move" << std::endl;
}
Foo(Foo const & x) {
std::cout << "copy" << std::endl;
}
~Foo() {
std::cout << "destructor" << std::endl;
}
};
Foo f(bool b) {
Foo x;
Foo y;
return b ? x : y;
}
int main() {
Foo x(f(true));
std::cout << "fin" << std::endl;
}
This yields
constructor
constructor
copy
destructor
destructor
fin
destructor
which makes sense. Now, I could force the move constructor to be called in the above code by changing the line
return b ? x : y;
to
return std::move(b ? x : y);
This gives the output
constructor
constructor
move
destructor
destructor
fin
destructor
However, I don't really like to call std::move directly.
Really, the issue is that I'm in a situation where I absolutely, positively, can not call the copy constructor even when the constructor exists. In my use case, there's too much memory to copy and although it'd be nice to just delete the copy constructor, it's not an option for a variety of reasons. At the same time, I'd like to return these objects from a function and would prefer to use the RVO. Now, I don't really want to have to remember all of the nuances of the RVO when coding and when it's applied an when it's not applied. Mostly, I want the object to be returned and I don't want the copy constructor called. Certainly, the RVO is better, but the move semantics are fine. Is there a way to the RVO when possible and the move semantics when not?
The following question helped me figure out what's going on. Basically, 12.8.32 of the standard states:
When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. —end note ]
Alright, so to figure out what the criteria for a copy elison are, we look at 12.8.31
in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
As such, if we define the code for f as:
Foo f(bool b) {
Foo x;
Foo y;
if(b) return x;
return y;
}
Then, each of our return values is an automatic object, so 12.8.31 says that it qualifies for copy elison. That kicks over to 12.8.32 which says that the copy is performed as if it were an rvalue. Now, the RVO doesn't happen because we don't know a priori which path to take, but the move constructor is called due to the requirements in 12.8.32. Technically, one move constructor is avoided when copying into x. Basically, when running, we get:
constructor
constructor
move
destructor
destructor
fin
destructor
Turning off elide on constructors generates:
constructor
constructor
move
destructor
destructor
move
destructor
fin
destructor
Now, say we go back to
Foo f(bool b) {
Foo x;
Foo y;
return b ? x : y;
}
We have to look at the semantics for the conditional operator in 5.16.4
If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category and it is a bit-field if the second or the third operand is a bit-field, or if both are bit-fields.
Since both x and y are lvalues, the conditional operator is an lvalue, but not an automatic object. Therefore, 12.8.32 doesn't kick in and we treat the return value as an lvalue and not an rvalue. This requires that the copy constructor be called. Hence, we get
constructor
constructor
copy
destructor
destructor
fin
destructor
Now, since the conditional operator in this case is basically copying out the value category, that means that the code
Foo f(bool b) {
return b ? Foo() : Foo();
}
will return an rvalue because both branches of the conditional operator are rvalues. We see this with:
constructor
fin
destructor
If we turning off elide on constructors, we see the moves
constructor
move
destructor
move
destructor
fin
destructor
Basically, the idea is that if we return an rvalue we'll call the move constructor. If we return an lvalue, we'll call the copy constructor. When we return a non-volatile automatic object whose type matches that of the return type, we return an rvalue. If we have a decent compiler, these copies and moves may be elided with the RVO. However, at the very least, we know what constructor is called in case the RVO can't be applied.
Compilers often perform Named Return Value Optimization (NRVO) in such cases, but it is not guaranteed.
In the context of the C++ programming language, return value optimization (RVO) is a compiler optimization that involves eliminating the temporary object created to hold a function's return value. RVO is allowed to change the observable behaviour of the resulting program by the C++ standard.
std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.
std::move is totally unnecessary when returning from a function, and really gets into the realm of you -- the programmer -- trying to babysit things that you should leave to the compiler.
When the expression in the return statement is a non-volatile automatic duration object, and not a function or catch-clause parameter, with the same cv-unqualified type as the function return type, the resulting copy/move is eligible for copy elision. The standard also goes on to say that, if the only reason copy elision was forbidden was that the source object was a function parameter, and if the compiler is unable to elide a copy, the overload resolution for the copy should be done as if the expression was an rvalue. Thus, it would prefer the move constructor.
OTOH, since you are using the ternary expression, none of the conditions hold and you are stuck with a regular copy. Changing your code to
if(b)
return x;
return y;
calls the move constructor.
Note that there is a distinction between RVO and copy elision - copy elision is what the standard allows, while RVO is a technique commonly used to elide copies in a subset of the cases where the standard allows copy elision.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With