I know that NRVO allows a function to construct an object and return that object by value without the cost of a copy or even move operation. It found that it also works with nested function calls, allowing you to construct the object from the return value of another function call.
Please consider the following program and it's output as shown in the comments:
(Output from Visual Studio 2017, version 15.2, release build.)
#include <stdio.h>
class W
{
public:
W() { printf( "W::W()\n" ); }
W( const W& ) { printf( "W::W( const W& )\n" ); }
W( W&& ) { printf( "W::W( W&& )\n" ); }
W& operator=( const W& ) { printf( "W::operator=( const W& )\n" ); }
W& operator=( W&& ) { printf( "W::operator=( W&& )\n" ); }
~W() { printf( "W::~W()\n" ); }
void Transform() { printf( "W::Transform()\n" ); }
void Run() { printf( "W::Run()\n" ); }
};
W make()
{
W w;
return w;
}
W transform_make()
{
W w{ make() };
w.Transform();
return w;
}
W transform1( W w )
{
w.Transform();
return w;
}
W&& transform2( W&& w )
{
w.Transform();
return std::move(w);
}
int main() // Program output:
{
printf( "TestM:\n" ); //TestM:
{ //W::W()
W w{ make() }; //W::Run()
w.Run(); //W::~W()
}
//TestTM:
printf( "TestTM:\n" ); //W::W()
{ //W::Transform()
W w{ transform_make() }; //W::Run()
w.Run(); //W::~W()
}
//TestT1:
printf( "TestT1:\n" ); //W::W()
{ //W::Transform()
W w{ transform1( make() ) }; //W::W( W&& )
w.Run(); //W::~W()
} //W::Run()
//W::~W()
printf( "TestT2:\n" ); //TestT2:
{ //W::W()
W&& w{ transform2( make() ) }; //W::Transform()
w.Run(); //W::~W()
} //W::Run()
}
TestM
is the normal NRVO case. The object W
is constructed and destructed only once.
TestTM
is the nested NRVO case. Again the object is constructed only once and never copied or moved. So far so good.
Now getting to my question - how can I make TestT1
work with the same efficiency as TestTM
? As you can see in TestT1
a second object is move constructed - this is something I would like to avoid. How can I change the function transform1()
to avoid any additional copies or moves? If you think about it, TestT1
is not that much different from TestTM
, so I have a feeling that this is something that must be possible.
For my second attempt, TestT2
, I tried passing the object via RValue reference. This eliminated the extra move constructor, but unfortunately this causes the destructor to be called before I am done with the object, which is not always ideal.
Update:
I also note that it is possible to make it work using references, as long as you make sure not to use the object beyond the end of the statement:
W&& transform2( W&& w )
{
w.Transform();
return std::move(w);
}
void run( W&& w )
{
w.Run();
}
printf( "TestT3:\n" ); //TestT3:
{ //W::W()
run( transform2( make() ) ); //W::Transform()
} //W::Run()
//W::~W()
Is this safe to do?
> Note also that C doesn't have return-value-optimization, hence all your struct-returning functions will cause a call to memcpy (won't happen when compiled in C++ mode of course).
(Named) Return value optimization is a common form of copy elision. It refers to the situation where an object returned by value from a method has its copy elided. The example set forth in the standard illustrates named return value optimization, since the object is named.
Compilers often perform Named Return Value Optimization (NRVO) in such cases, but it is not guaranteed.
C++ Copy Elision Purpose of copy elision Copy elision (sometimes called return value optimization) is an optimization whereby, under certain specific circumstances, a compiler is permitted to avoid the copy or move even though the standard says that it must happen.
This happens in Test1
because the compiler is explicitly disallowed to apply NRVO from by value parameters from a function's argument list. And in Test1
you are accepting a W
instance by value as a function parameter, so the compiler cannot elide the move on return.
See Why are by-value parameters excluded from NRVO? and my discussion with Howard Hinnant about the issue here Why does for_each return function by move in the comments
You cannot make Test1
work as efficiently as you did in the earlier case because of this.
The relevant quote from the standard
15.8.3 Copy/move elision [class.copy.elision]
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, ...
- 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 parameter or a variable introduced by the exception-declaration of a handler (18.3)) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function call’s return object
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