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.
Related
For std::atomic the copy constructor is deleted, and this should only compile with C++17 and higher due to copy elision:
std::atomic<int> t_int = 1;
I expected that it does not compile using -fno-elide-constructors flag, but it still compiles:
https://godbolt.org/z/nMvG5vTrK
Why is this?
C++17 doesn't simply say that the previously optional return value optimizations are now mandatory. The actual description of the language changed so that there is no creation of a temporary object anymore in the first place.
So, since C++17, there is no constructor call that could be elided anymore. Hence it makes sense that -fno-elide-constructors doesn't add any temporary creation. That would be against the language rules.
Before C++17 the language describes that a temporary object is created from which the variable is initialized and then adds that a compiler is allowed to elide this temporary. Therefore, whether -fno-elide-constructors is used or not, the compiler is behaving standard compliant by eliding or not eliding the temporary copy.
I expected that it does not compile using -fno-elide-constructors flag,
The given flag has no effect on return value optimization(aka RVO) from C++17 and onwards. This is because it is not considered an optimization anymore(from C++17) but instead it is a language guarantee.
From mandatory copy elison:
Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:
In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:
(emphasis mine)
This means that t_int is constructed directly from the prvalue 1. And the flag -fno-elide-constructors has no effect on RVO which is different from NRVO. And so the copy constructor being deleted has no effect in your case.
Perhaps an example might help illustrating the same,
struct Custom
{
public:
Custom(int p): m_p(p)
{
std::cout<<"converting ctor called"<<std::endl;
}
Custom(const Custom&) = delete; //deleted copy ctor
private:
int m_p = 0;
};
int main()
{
Custom c = 1; //works in C++17 but not in Pre-C++17
}
Since C++17, the meaning of prvalue has changed, which makes copy elision guaranteed in some cases. From cppreference, the copy/move constructors need not be present or accessible in that case.
When an exception is thrown, the exception object is copy-initialized, and the copy/move may be subject to copy elision. But is it required that the copy/move constructor must be available at this time?
From [except.throw]:
When the thrown object is a class object, the constructor selected for the copy-initialization as well as the constructor selected for a copy-initialization considering the thrown object as an lvalue shall be non-deleted and accessible, even if the copy/move operation is elided ([class.copy.elision]). The destructor is potentially invoked ([class.dtor]).
The standard mentions that the corresponding constructor must be non-deleted and accessible, even if it is elided.
However, I tested and found that both GCC and Clang allow to throw when the corresponding constructor is deleted:
struct A {
A() = default;
A(A&&) = delete;
};
try {
throw A{}; // copy elision
} catch(...) {}
The code compiles, but it does not seem to meet the requirements of the standard. If I lower the version from C++17 to C++14, they will both report errors:
struct A {
A() = default;
A(A&&) = delete;
};
try {
throw A{}; // error: call to deleted constructor of 'A'
} catch(...) {}
Is my understanding of the standard wrong, or is it just the compiler relaxing the restrictions?
Your understanding of the standard is correct, in so far as your analysis of that paragraph actually applies.
But it doesn't apply, because no constructor is ever considered for copy-initialization.
Guaranteed elision is something of a misnomer; it's a useful explanation of the concept, but it doesn't reflect exactly how it works as far as the standard is concerned. Guaranteed elision works by rewriting the meaning of prvalues so there never is a copy/move to be elided in the first place.
A a = A{}; is copy-initialization, but it doesn't even hypothetically call a copy/move constructor. The variable a is initialized by the prvalue's initializer:
The result of a prvalue is the value that the expression stores into its context. A prvalue whose result is the value V is sometimes said to have or name the value V. The result object of a prvalue is the object initialized by the prvalue
a is the "result object" initialized by the prvalue.
The same goes here. A{} is a prvalue. The exception object is the "result object" to be initialized by the prvalue. There's no temporary object, no copy/move constructors that ever get considered for usage.
Firstly, C++17 guarantees copy elision neither when creating an exception object nor when activating an exception handler.
The requirement for the copy constructors to exist actually is simple - on the one hand, there are some cases when copy elision is just not sensible, including the both throwing and catching exceptions; on the other hand compilers are only permitted to perform the elision but not obligated. So the compiler would use the constructors in the cases.
The fact that your code does not implement the cases does not mean that your types (classes) are permitted to not support the cases (even though a compiler can successfully compile it). The cases are still valid for the language and must be implementable without modifying code your types (classes) used for exception.
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.
Assuming there is no compiler optimization. How many times would OutputBuffer_s type object will be created?
#include <iostream>
#include <vector>
struct OutputBuffer_s {
int encoded[10];
};
OutputBuffer_s func() {
OutputBuffer_s s;
return s;
}
int main() {
OutputBuffer_s a = func();
}
Initially, I had assumed three times.
1) When func() is called, object s will be created on stack.
2) When func() goes out of scope, it will return copy of object s to main().
3) Copying of value to object a in main(), since value returned by func() would be a temporary.
I know that I'm wrong here, since I compiled with -O0 in g++ but I could see only one creation after overriding the constructors. I want to know where and why I am wrong.
What you have here copy-elison.
Omits copy and move (since C++11) constructors, resulting in zero-copy pass-by-value semantics.
GCC can elide the constructors even with -O0 option. This is what is happening here. If you want to specifically prevent elision, you can use the -fno-elide-constructors option.
If you use this option, there will be one constructor call and two move constructor calls for C++11.
See demo here.
If you use C++17, there is guaranteed copy-elision in some cases, and here even with the -fno-elide-constructors option, there will be one constructor call and just one move constructor call.
See demo here.
C++17 has introduced Temporary materialization which I quote:
A prvalue of any complete type T can be converted to an xvalue of the same type T. This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object. If T is a class or array of class type, it must have an accessible and non-deleted destructor.
In that case the extra calls to the contructor will become a move operation. Prior to C++17, which copy elision was not mandatory, the compiler would usually copy elide. As far as I am aware, in your case, a compiler would copy elide anyway (try godbolt and check the produced assembly).
To fully answer, one call to the constructor and one move.
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.