I use the code below to test copy elision:
class foo
{
public:
foo() {cout<<"ctor"<<endl;};
foo(const foo &rhs) {cout<<"copy ctor"<<endl;}
};
int g(foo a)
{
return 0;
}
int main()
{
foo a;
g(std::move(a));
return 0;
}
I expected only the default constructor would be called because the argument of g()
is an rvalue and copy will be elided. But the result shows that both the default constructor and the copy constructor are called. Why?
And if I change the function call to g(foo())
, the copy will be elided. What's the difference between the return types of foo()
and std::move(a)
? How can I make the compiler elide copy on an lvalue?
Copy elision for can only occur in a few specific situations, the most common of which is the copying of a temporary (the others are returning locals, and throwing/catching exceptions). There is no temporary being produced by your code, so no copy is elided.
The copy constructor is being called because foo
does not have a move constructor (move constructors are not implicitly generated for classes with explicit copy constructors), and so std::move(a)
matches the foo(const foo &rhs)
constructor (which is used to construct the function argument).
A copy of an lvalue can be elided in the following situations (although there is no way to force a compiler to perform the elision):
foo fn() {
foo localAutomaticVariable;
return localAutomaticVariable; //Copy to construct return value may be elided
}
int main() {
try {
foo localVariable;
throw localVariable; //The copy to construct the exception may be elided
}
catch(...) {}
}
If you want to avoid copies when passing function arguments, you can use a move constructor which pilfers the resources of the objects given to it:
class bar {
public:
bar() {cout<<"ctor"<<endl;};
bar(const bar &rhs) {cout<<"copy ctor"<<endl;}
bar(bar &&rhs) {cout<<"move ctor"<<endl;}
};
void fn(bar a)
{
}
//Prints:
//"ctor"
//"move ctor"
int main()
{
bar b;
f(std::move(b));
}
Also, whenever copy elision is allowed but does not occur, the move constructor will be used if it is available.
You need to declare g
as:
int g(foo && a) //accept argument as rvalue reference
{
return 0;
}
Now it can accept argument by rvalue-reference.
In your case, even though the expression std::move(a)
produces rvalue, it doesn't bind to a parameter which accepts argument by value. The receiving end must be rvalue-reference as well.
In case of g(foo())
, the copy-elision is performed by the compiler, which is an optimization. It is NOT a requirement by the language[until C++17]. You can disable this optimization if you want to : then g(foo())
and g(std::move(a))
will behave exactly same, as expected.
But if you change g
as I suggested above, the call g(foo())
will not make a copy because it is a requirement by the language to not make copy with &&. It is not a compiler-optimization anymore.
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