Skimming through the new C ++ 11 standard, I decided to deepen my understanding in the topic of rvalue references. Everything is, in principle, wonderful, but there are pitfalls, namely: some loss of backward compatibility with C ++ 03.
The standard allows the compiler (but not obliging it) to treat an expression passed to the return operator as an rvalue reference and implement move semantics, even if it is not a temporary object. For example:
std :: string f ( )
{
std :: string s = "Hi!" ;
return s ;
}
Here, before return s there was an lvalue, and return perceives it already as an rvalue reference. And this is great, because this gives a good performance boost when returning heavy objects, it is not copying, but moving the internal state of the line. Let's try to understand why the standard allows the return operator to treat any expression whose result lives on a function's stack area, like rvalue reference? The answer is obvious: yes, because after return the result of this expression is no longer needed, even if the expression is a named object, such an object is xvalue (xPiring value), you can safely move.
It is important to focus on the words
"no longer needed .
" Is it possible to guarantee that after return and before returning from a function, no code will be called? For C, you can (SEH and __try ...__ finally is not a standard), for C ++ it is impossible. After executing the return and before returning from the function, destructors of automatic objects will be called, starting with the current scope, ending with the scope of the function.
// The class is initialized with a string reference, the destructor outputs it to the console.
struct finalizer
{
Finalizer ( std :: string const & str )
: _str ( str )
{
}
')
~ Finalizer ( )
{
std :: cout << _str << std :: endl ;
}
private :
std :: string const & _str ;
} ;
std :: string f ( )
{
// break NRVO (checked only on MSVC 10.0)
if ( false )
Return std :: string ( "Obviously impossible code!" ) ;
// -------------------------------------------
std :: string s = "Hi!" ;
Finalizer fin ( s ) ;
return s ;
}
The first 2 lines of fictitious code are needed to make it impossible to apply NRVO optimization (for MSVC, there are no other compilers at hand to check, sure to bypass NRVO in your favorite compiler will also be easy). The C ++ 03 compiler will compile the code in which function f will print the string “Hi!”, And C ++ 11 (as everyone guessed) will print an empty string, because return dragged off the contents of string s.
I conducted tests on MSVC 9.0 (c ++ 03) and 10.0 (partial c ++ 11). Your C ++ 11 compiler can theoretically output the string “Hi!” In my example. The fact is that most compilers use optimization for storing short strings. For this, the std :: string class stores a small fixed buffer inside itself, and if the string fits in it, the heap is not used, which means there is nothing to move, and the donor string can theoretically not be changed, and it will be even slightly more productive. In such a case, make the greeting line longer. This behavior will become even more unexpected at runtime, when short strings are output, but long ones are not. This is theoretically. I didn’t notice in the standard the requirements for the implementation of move semantics for std :: string, is the donor string obligated to fulfill the s.empty () == true post-condition? If there is such a thing - correct me.
An example is contrived, but simple to consider. In practice, I have never encountered such a problem of transferring code to a new standard. I can only assume that the risk group is a code that uses BOOST_SCOPE_EXIT (a handy thing, you had to use it several times), which is designed to execute the final code through a call to the destructor class, which (class) is initialized with links to local variables. Yes, and handwritten finalizers, I also had the honor to see.
In any case, this is some kind of sacrifice of backward compatibility in favor of a fairly powerful increase in performance. Here we gain much more than we lose.
I hope the article will help to foresee such a situation, save the potential lucky person who has caught it from unnecessary nerves and keep a couple of extra hairs on his head when searching for a glitch, moving to a new compiler.
Everyone has a pleasant transition to C ++ 11.
UPD: Included in the example is a dummy code that allows you to bypass the NRVO for MSVC.