Assignment of a temporary object vs. move assignment [duplicate] - c++

In many cases when returning a local from a function, RVO (return value optimization) kicks in. However, I thought that explicitly using std::move would at least enforce moving when RVO does not happen, but that RVO is still applied when possible. However, it seems that this is not the case.
#include "iostream"
class HeavyWeight
{
public:
HeavyWeight()
{
std::cout << "ctor" << std::endl;
}
HeavyWeight(const HeavyWeight& other)
{
std::cout << "copy" << std::endl;
}
HeavyWeight(HeavyWeight&& other)
{
std::cout << "move" << std::endl;
}
};
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return heavy;
}
int main()
{
auto heavy = MakeHeavy();
return 0;
}
I tested this code with VC++11 and GCC 4.71, debug and release (-O2) config. The copy ctor is never called. The move ctor is only called by VC++11 in debug config. Actually, everything seems to be fine with these compilers in particular, but to my knowledge, RVO is optional.
However, if I explicitly use move:
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return std::move(heavy);
}
the move ctor is always called. So trying to make it "safe" makes it worse.
My questions are:
Why does std::move prevent RVO?
When is it better to "hope for the best" and rely on RVO, and when should I explicitly use std::move? Or, in other words, how can I let the compiler optimization do its work and still enforce move if RVO is not applied?

The cases where copy and move elision is allowed is found in section 12.8 §31 of the Standard (version N3690):
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
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 cv-unqualified 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
[...]
when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
[...]
(The two cases I left out refer to the case of throwing and catching exception objects which I consider less important for optimization.)
Hence in a return statement copy elision can only occur, if the expression is the name of a local variable. If you write std::move(var), then it is not the name of a variable anymore. Therefore the compiler cannot elide the move, if it should conform to the standard.
Stephan T. Lavavej talked about this at Going Native 2013 (Alternative source) and explained exactly your situation and why to avoid std::move() here. Start watching at minute 38:04. Basically, when returning a local variable of the return type then it is usually treated as an rvalue hence enabling move by default.

how can I let the compiler optimization do its work and still enforce move if RVO is not applied?
Like this:
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return heavy;
}
Transforming the return into a move is mandatory.

Related

copy elision in c++03

Copy-elision is, in some cases, mandatory in c++17, and permitted in c++11/14. This in particular concerns copy initialization.
For example, the following program
#include <iostream>
struct A
{
explicit A(int){ std::cout << "conversion" << std::endl; }
A(const A&) { std::cout << "copy constructor" << std::endl; }
};
int main()
{
A b = A(3);
}
is expected in c++17 to produce an output
conversion
and in c++11/14 may produce the same output. With these regards, both gcc 10.1.0 and clang 11.1.0 produce the output above also with -std=c++11 or -std=c++14, unless one explicitly disables the optional constructors elision with -fno-elide-constructors.
But what about c++03 standard? Was it allowed to elide the copy constructors in the copy initialization? gcc and clang with -std=c++03 always suppress the copy constructor (unless one specifies -fno-elide-constructors).
Yes, copy ellision is permitted in C++03 and C++98. That's the paragraph for C++98 and C++03:
Non-mandatory elision of copy operations
Under the following circumstances, the compilers are permitted, but
not required to omit the copy construction of
class objects even if the copy constructor and the
destructor have observable side-effects. The objects are constructed
directly into the storage where they would otherwise be copied
to. This is an optimization: even when it takes place and the
copy constructor is not called, it still must be
present and accessible (as if no optimization happened at all),
otherwise the program is ill-formed:
In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a
function parameter or a catch clause parameter, and which is of the
same class type (ignoring cv-qualification) as the function return
type. This variant of copy elision is known as NRVO, "named return
value optimization".
In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring
cv-qualification) as the target object. When the nameless temporary is
the operand of a return statement, this variant of copy elision is
known as RVO, "return value optimization".
When copy elision occurs, the implementation treats the source and target of the omitted copy operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization
cppreference
I removed everything that's only valid since C++11.
The only differences between C++98, C++03 and C++11 regarding ellision are move operations and exception handling.

Disable copy elision in C++

Disclaimer: Goal of research is how to disable copy elision and return value optimization for supplied part of code. Please avoid from answering if want to mention something like XY-problem. The question has strictly technical and research character and is formulated strongly in this way
In C++14 there was introduced copy elision and return value optimization. If some object had been destructed and copy-constructed in one expression, like copy-assignment or return immediate value from function by value, copy-constructor is elided.
Following reasoning is applied to copy constructor, but similar reasoning can be performed for move constructor, so this is not considered further.
There are some partial solutions for disabling copy elision for custom code:
1) Compiler-dependent option. For GCC, there is solution based on __attribule__ or #pragma GCC constructions, like this https://stackoverflow.com/a/33475393/7878274 . But since it compiler-dependent, it does not met question.
2) Force-disabling copy-constructor, like Clazz(const Clazz&) = delete. Or declare copy-constructor as explicit to prevent it's using. Such solution does not met task since it changes copy-semantics and forces introducing custom-name functions like Class::copy(const Clazz&).
3) Using intermediate type, like describe here https://stackoverflow.com/a/16238053/7878274 . Since this solution forces to introduce new descendant type, it does not met question.
After some research there was found that reviving temporary value can solve question. If reinterpret source class as reference to one-element array with this class and extract first element, then copy elision will turned off. Template function can be written like this:
template<typename T, typename ... Args> T noelide(Args ... args) {
return (((T(&)[1])(T(args...)))[0]);
}
Such solution works good in most cases. In following code it generates three copy-constructor invocations - one for direct copy-assignment and two for assignment with return from function. It works good in MSVC 2017
#include <iostream>
class Clazz {
public: int q;
Clazz(int q) : q(q) { std::cout << "Default constructor " << q << std::endl; }
Clazz(const Clazz& cl) : q(cl.q) { std::cout << "Copy constructor " << q << std::endl; }
~Clazz() { std::cout << "Destructor " << q << std::endl; }
};
template<typename T, typename ... Args> T noelide(Args ... args) {
return (((T(&)[1])(T(args...)))[0]);
}
Clazz func(int q) {
return noelide<Clazz>(q);
}
int main() {
Clazz a = noelide<Clazz>(10);
Clazz b = func(20);
const Clazz& c = func(30);
return 0;
}
This approach works good for a and b cases, but performs redundant copy with case c - instead of copy, reference to temporary should be returned with lifetime expansion.
Question: how to modify noelide template to allow it work fine with const lvalue-reference with lifetime expansion?
Thanks!
According to N4140, 12.8.31:
...
This elision of copy/move operations, called copy elision, is
permitted in the following circumstances (which may be combined to
eliminate multiple copies):
(31.1) — 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
cv-unqualified 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
(31.3) — when a temporary class object that has not been bound to a
reference (12.2) would be copied/moved to a class object with the same
cv-unqualified type, the copy/move operation can be omitted by
constructing the temporary object directly into the target of the
omitted copy/move
So if I understand it correctly, copy elision can only occur, if the return statement is a name of a local variable. So you can for example 'disable' copy elision by returning e.g. return std::move(value)... If you don't like using move for this, you can simply implement noelide as a static_cast<T&&>(...).
This is impossible to be done given all your restrictions. Simply, because the standard does not provide a way of turning off RVO optimizations.
You can prevent mandatory application of RVO by breaking one of the requirements, but you cannot reliably prevent optional allowed optimization. Everything you do is either changing semantics or compiler specific at this point (e.g. -fno-elide-constructors option for GCC and Clang).

GCC NRVO/RVO warning

Is there any warning, which allows us to know whether NRVO/RVO performed or not, in GCC?
I found that -fno-elide-constructors turns off NRVO/RVO, but NRVO/RVO has its own conditions to occur and sometimes does not occur. There is a need to know if NRVO/RVO occurs to understand, when extra copy-construction happens.
I am especially interested in compile-time features. It would be nice if there were some specific #pragma GCC... (which activates the diagnostic immediately following itself) or something using static assertion mechanism.
I am not aware of any gcc specific diagnostic message or other method that easily can solve your task. As you have found out, -fno-elide-constructors will disable copy/move elisions, so you will know for sure that (N)RVO will not happen in that case at least.
However, a quick look at paragraph 31 in section 12.8 of this C++11 working draft states that:
When certain criteria are met, an implementation is allowed to omit
the copy/move construction of a class object, even if the copy/move
constructor and/or destructor for the object have side effects. In
such cases, the implementation treats the source and target of the
omitted copy/move operation as simply two different ways of referring
to the same object, and the destruction of that object occurs at the
later of the times when the two objects would have been destroyed
without the optimization. This elision of copy/move operations,
called copy elision, is permitted in the following circumstances
(which may be combined to eliminate multiple copies):
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 cv-unqualified
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
...
when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same
cv-unqualified type, the copy/move operation can be omitted by
constructing the temporary object directly into the target of the
omitted copy/move
...
When copy/move elision happen the local auto object is the same as the temporary (return) object, which in turn is the same as the "storage" object (where the return value is stored). So the local auto object is the same as the storage object, which means a pointer comparison will equal true. A simple example to demonstrate this:
#include <iostream>
#include <vector>
std::vector<int> testNRVO(int value, size_t size, const std::vector<int> **localVec)
{
std::vector<int> vec(size, value);
*localVec = &vec;
/* Do something here.. */
return vec;
}
int main()
{
const std::vector<int> *localVec = nullptr;
std::vector<int> vec = testNRVO(0, 10, &localVec);
if (&vec == localVec)
std::cout << "NRVO was applied" << std::endl;
else
std::cout << "NRVO was not applied" << std::endl;
}
Enabling/disabling -fno-elide-constructors changes the printed message as expected. Note: in the strictest sense the pointer comparison might be depending on undefined behavior when (N)RVO does not happen, since the local auto object is non-existing.
Doing pointer comparisons will add cruft, but with the advantage of compiler-independency.

Why does std::move prevent RVO (return value optimization)?

In many cases when returning a local from a function, RVO (return value optimization) kicks in. However, I thought that explicitly using std::move would at least enforce moving when RVO does not happen, but that RVO is still applied when possible. However, it seems that this is not the case.
#include "iostream"
class HeavyWeight
{
public:
HeavyWeight()
{
std::cout << "ctor" << std::endl;
}
HeavyWeight(const HeavyWeight& other)
{
std::cout << "copy" << std::endl;
}
HeavyWeight(HeavyWeight&& other)
{
std::cout << "move" << std::endl;
}
};
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return heavy;
}
int main()
{
auto heavy = MakeHeavy();
return 0;
}
I tested this code with VC++11 and GCC 4.71, debug and release (-O2) config. The copy ctor is never called. The move ctor is only called by VC++11 in debug config. Actually, everything seems to be fine with these compilers in particular, but to my knowledge, RVO is optional.
However, if I explicitly use move:
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return std::move(heavy);
}
the move ctor is always called. So trying to make it "safe" makes it worse.
My questions are:
Why does std::move prevent RVO?
When is it better to "hope for the best" and rely on RVO, and when should I explicitly use std::move? Or, in other words, how can I let the compiler optimization do its work and still enforce move if RVO is not applied?
The cases where copy and move elision is allowed is found in section 12.8 §31 of the Standard (version N3690):
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
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 cv-unqualified 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
[...]
when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
[...]
(The two cases I left out refer to the case of throwing and catching exception objects which I consider less important for optimization.)
Hence in a return statement copy elision can only occur, if the expression is the name of a local variable. If you write std::move(var), then it is not the name of a variable anymore. Therefore the compiler cannot elide the move, if it should conform to the standard.
Stephan T. Lavavej talked about this at Going Native 2013 (Alternative source) and explained exactly your situation and why to avoid std::move() here. Start watching at minute 38:04. Basically, when returning a local variable of the return type then it is usually treated as an rvalue hence enabling move by default.
how can I let the compiler optimization do its work and still enforce move if RVO is not applied?
Like this:
HeavyWeight MakeHeavy()
{
HeavyWeight heavy;
return heavy;
}
Transforming the return into a move is mandatory.

Copy-elision of automatic variable for return

I am wondering if in C++0x "12.8 Copying and Moving class objects [class.copy] paragraph 31" when copy elision happens, exactly:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object [...]. This elision of copy/move
operations, called copy elision, is permitted in the following circumstances [...]:
in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object [...] with the same cv-unqualified 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
[...]
And now I wonder, if this allows in the following code to elude copy
vector<string> gen(const char *fn) {
if(fn == nullptr) // this should prevent RVO
return {"House", "Horse", "Hen"};
vector<string> res;
fillFromFile(res, fn);
return res; // copy elision possible?
}
int main() {
vector<string> data = gen("users.dat");
}
Or does that rule not fot the example, and I have to do it explicit?
return move(res); // explicitly prevent copy
Note that my intention of the if was to eliminate the obvious Return Value Optimization (RVO).
Or am I completely on the wrong track here? There was a change involving return and move that could use rvalue references, right?
Yes, copy elision is possible/allowed in both cases.
In compiler terminology, the two cases are slightly different though. return {"House", "Horse", "Hen"}; constructs an unnamed object, so regular RVO kicks in.
return res; is slightly more complex, because you are returning a named object which was already constructed earlier. This optimization is typically called NRVO (Named Return Value Optimization), and it is slightly less common for compilers to implement it.
MSVC always implements RVO, and performs NRVO in release builds.
I believe recent versions of GCC always perform both RVO and NRVO.
By the way, I don't really see why your ´if` would make a difference for RVO.
Yes, the compiler has specific instructions to treat res like an rvalue in this context, and res will be moved into data. Of course, the compiler could easily apply RVO/NRVO here anyway because it can statically determine that you never call the function with nullptr, and in addition, the function could be trivially transformed so that RVO/NRVO can be applied even if that couldn't be proven, and finally, that doesn't even prevent RVO/NRVO as the result can still be constructed in.