Reputation: 2671
I have understood from several questions here on SO that (N)RVO prevents the move constructor from being called, when an object is returned by value. Classic example:
struct Foo {
Foo() { std::cout << "Constructed\n"; }
Foo(const Foo &) { std::cout << "Copy-constructed\n"; }
Foo(Foo &&) { std::cout << "Move-constructed\n"; }
~Foo() { std::cout << "Destructed\n"; }
};
Foo makeFoo() {
return Foo();
}
int main() {
Foo foo = makeFoo(); // Move-constructor would be called here without (N)RVO
}
The output with (N)RVO enabled is:
Constructed
Destructed
So in what cases the move constructor would be called, regardless the (N)RVO presence? Could you provide some examples? In other words: why should I care implementing a move constructor if the (N)RVO does its optimization job by default?
Upvotes: 2
Views: 674
Reputation: 15334
First, you should probably make sure that Foo
follows the rule of three/five and has move/copy assignment operators. And it is good practice for the move-constructor and move-assignment operator to be noexcept
:
struct Foo {
Foo() { std::cout << "Constructed\n"; }
Foo(const Foo &) { std::cout << "Copy-constructed\n"; }
Foo& operator=(const Foo&) { std::cout << "Copy-assigned\n"; return *this; }
Foo(Foo &&) noexcept { std::cout << "Move-constructed\n"; }
Foo& operator=(Foo &&) noexcept { std::cout << "Move-assigned\n"; return *this; }
~Foo() { std::cout << "Destructed\n"; }
};
In most cases you can follow the rule of zero and don't actually need to define any of these special member functions, the compiler will create them for you, but it is useful for this purpose.
(N)RVO is only for function return values. It does not apply, for example, for function parameters. Of course the compiler can apply whatever optimizations it likes under the "as-if" rule so we have to be careful when crafting trivial examples.
There are many cases where the move-constructor or move-assignment operator will be called. But a simple case is if you use std::move
to transfer ownership to a function that accepts a parameter by-value or by rvalue-reference:
void takeFoo(Foo foo) {
// use foo...
}
int main() {
Foo foo = makeFoo();
// set data on foo...
takeFoo(std::move(foo));
}
Constructed
Move-constructed
Destructed
Destructed
A very useful case for the move-constructor is if you have a std::vector<Foo>
. As you push_back
objects into the container it occasionally has to re-allocate and move all the existing objects to new memory. If there is a valid move-constructor available on Foo
it will use it instead of copying:
int main() {
std::vector<Foo> v;
std::cout << "-- push_back 1 --\n";
v.push_back(makeFoo());
std::cout << "-- push_back 2 --\n";
v.push_back(makeFoo());
}
-- push_back 1 --
Constructed
Move-constructed <-- move new foo into container
Destructed
-- push_back 2 --
Constructed
Move-constructed <-- move existing foo to new memory
Move-constructed <-- move new foo into container
Destructed
Destructed
Destructed
Destructed
I find move-constructors useful in constructor member initializer lists. Say you have a class FooHolder
that contains a Foo
. Then you can define a constructor that takes a Foo
by-value and moves it into the member variable:
class FooHolder {
Foo foo_;
public:
FooHolder(Foo foo) : foo_(std::move(foo)) {}
};
int main() {
FooHolder fooHolder(makeFoo());
}
Constructed
Move-constructed
Destructed
Destructed
This is nice because it allows me to define a constructor that accepts lvalues or rvalues without unnecessary copies.
RVO always applies but there are cases that defeat NVRO. For example if you have two named variables and the choice of return variable is not known at compile time:
Foo makeFoo(double value) {
Foo f1;
Foo f2;
if (value > 0.5)
return f1;
return f2;
}
Foo foo = makeFoo(value);
Constructed
Constructed
Move-constructed
Destructed
Destructed
Destructed
Or if the return variable is also a function parameter:
Foo appendToFoo(Foo foo) {
// append to foo...
return foo;
}
int main() {
Foo f1;
Foo f2 = appendToFoo(f1);
}
Constructed
Copy-constructed
Move-constructed
Destructed
Destructed
Destructed
One case for a move-assignment operator is if you want to optimize a setter for rvalues. Say you have a FooHolder
that contains a Foo
and you want a setFoo
member function. Then if you want to optimize for both lvalues and rvalues you should have two overloads. One that takes a reference-to-const and another that takes an rvalue-reference:
class FooHolder {
Foo foo_;
public:
void setFoo(const Foo& foo) { foo_ = foo; }
void setFoo(Foo&& foo) { foo_ = std::move(foo); }
};
int main() {
FooHolder fooHolder;
Foo f;
fooHolder.setFoo(f); // lvalue
fooHolder.setFoo(makeFoo()); // rvalue
}
Constructed
Constructed
Copy-assigned <-- setFoo with lvalue
Constructed
Move-assigned <-- setFoo with rvalue
Destructed
Destructed
Destructed
Upvotes: 4