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).
Related
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.
Why does the ternary operator prevent Return-Value Optimization (RVO) in MSVC? Consider the following complete example program:
#include <iostream>
struct Example
{
Example(int) {}
Example(Example const &) { std::cout << "copy\n"; }
};
Example FunctionUsingIf(int i)
{
if (i == 1)
return Example(1);
else
return Example(2);
}
Example FunctionUsingTernaryOperator(int i)
{
return (i == 1) ? Example(1) : Example(2);
}
int main()
{
std::cout << "using if:\n";
Example obj1 = FunctionUsingIf(0);
std::cout << "using ternary operator:\n";
Example obj2 = FunctionUsingTernaryOperator(0);
}
Compiled like this with VC 2013: cl /nologo /EHsc /Za /W4 /O2 stackoverflow.cpp
Output:
using if:
using ternary operator:
copy
So apparently the ternary operator somehow prevents RVO. Why? Why would the compiler not be clever enough to see that the function using the ternary operator does the same thing as the one using the if statement, and optimize accordingly?
Looking at the program output, it seems to me that, indeed, the compiler is eliding in both cases, why?
Because, if no elide was activated, the correct output would be:
construct the example object at function return;
copy it to a temporary;
copy the temporary to the object defined in main function.
So, I would expect, at least 2 "copy" output in my screen. Indeed, If I execute your program, compiled with g++, with -fno-elide-constructor, I got 2 copy messages from each function.
Interesting enough, If I do the same with clang, I got 3 "copy" message when the function FunctionUsingTernaryOperator(0); is called and, I guess, this is due how the ternary is implemented by the compiler. I guess it is generating a temporary to solve the ternary operator and copying this temporary to the return statement.
This related question contains the answer.
The standard says when copy or move elision is allowed in a return statement: (12.8.31)
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 cvunqualified 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
So basically copy elision only occures in the following cases:
returning a named object.
returning a temporary object
If your expression is not a named object or a temporary, you fall back to copy.
Some Interesting behaviors:
return (name); does not prevent copy elision (see this question)
return true?name:name; should prevent copy elision but gcc 4.6 at least is wrong on this one (cf. this question)
EDIT:
I left my original answer above but Christian Hackl is correct in his comment, it does not answer the question.
As far as the rules are concerned, the ternary operator in the example yields a temporary object so 12.8.31 allows the copy/move to be elided. So from the C++ language point of view. The compiler is perfectly allowed to elide the copy when returning from FunctionUsingTernaryOperator.
Now obviously the elision is not done. I suppose the only reason is that Visual Studio Compiler team simply did not implement it yet. And because in theory they could, maybe in a future release they will.
I can see that it violates one general rule about RVO – that the return object (should) be defined at a single position.
The snippet below satisfies the rule:
Example e;
e = (i == 1)? Example{1} : Example{2};
return e;
But in the original expression like below, two Example objects are defined at two different positions according to MSVC:
return (i == 1) ? Example(1) : Example(2);
While the conversion between the two snippets is trivial to humans, I can imagine that it doesn't automatically happen in the compiler without a dedicated implementation. In other words, it's a corner case which is technically RVO-able but the devs didn't realize.
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.
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.
if I compile (under G++) and run the following code it prints "Foo::Foo(int)". However after making copy constructor and assignment operators private, it fails to compile with the following error: "error: ‘Foo::Foo(const Foo&)’ is private". How comes it needs a copy constructor if it only calls standard constructor at runtime?
#include <iostream>
using namespace std;
struct Foo {
Foo(int x) {
cout << __PRETTY_FUNCTION__ << endl;
}
Foo(const Foo& f) {
cout << __PRETTY_FUNCTION__ << endl;
}
Foo& operator=(const Foo& f) {
cout << __PRETTY_FUNCTION__ << endl;
return *this;
}
};
int main() {
Foo f = Foo(3);
}
The copy constructor is used here:
Foo f = Foo(3);
This is equivalent to:
Foo f( Foo(3) );
where the first set of parens a re a call to the copy constructor. You can avoid this by saying:
Foo f(3);
Note that the compiler may choose to optimise away the copy constructor call, but the copy constructor must still be available (i.e not private). The C++ Standard specifically allows this optimisation (see section 12.8/15), no matter what an implementation of the copy constructor actually does.
What you see, is a result of allowed by standard optimization, when compiler avoids creation of temporary. Compiler is allowed to replace construction and assignment with simple construction even in presence of side effects (like IO in your example).
But fact if program is ill-formed or not should not depend on situation, when compiler makes this optimization or not. That's why
Foo f = Foo(3);
requires copy constructor. And
Foo f(3);
does not. Though it will probably lead to same binary code.
Quote from 12.8.15
When certain criteria are met, an
implementation is allowed to omit the
copy construction of a class object,
even if the copy constructor and/or
destructor for the object have side
effects. In such cases, 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.111) This
elision of copy operations 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 with the same
cv-unqualified type as the function
return type, the copy 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 to a
class object with the same
cv-unqualified type, the copy
operation can be omitted by
constructing the temporary object
directly into the target of the
omitted copy
See also "Return value optimization".