I have the following:
#include <iostream>
#include <utility>
class T {
public:
T() {std::cout << "Default constructor called" << std::endl;}
T(const T&) {std::cout << "Copy constructor called" << std::endl;}
};
static T s_t;
T foo() {
return s_t;
}
class A {
public:
A() = delete;
A(T t) : _t(t) {}
private:
T _t;
};
int main() {
std::cout << "Starting main" << std::endl;
A a(foo());
return 0;
}
When I compile it with the line:
g++ -std=c++17 -O3 test.cpp and run it, I get the following output
Default constructor called
Starting main
Copy constructor called
Copy constructor called
My question is: since the constructor of A is taking an r-value object of type T that is only used to initialize _t, is it possible for the compiler to avoid the second copy operation and just copy s_t directly into _t?
(I understand that I can potentially replace the second copy with a move by implementing a move constructor for T)
Your copy constructor has an observable side effect (output). The (almost) golden rule of C++ optimizations is the as-if rule: Observable side effects must not be changed. But one of the two exceptions from this rule is the elision of some copy operations, specified here:
http://eel.is/c++draft/class.copy.elision
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.
But this is only allowed in very specific circumstances:
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 [...]
Your variable does not have automatic storage duration (i.e. it is not a local variable).
in a throw-expression, [...]
We are not a throw-expression.
in a coroutine [...]
We are not in a coroutine.
when the exception-declaration of an exception handler ([except]) declares an object of the same type (except for cv-qualification) as the exception object ([except.throw]) [...]
This is not the case either.
Neither the return of a global variable from a function, nor the use of a constructor parameter to initialize a member variable is covered here, so it would be illegal for any compiler to elide these copies in the code you show.
That said, if the compiler can prove that there are no side effects (which, in the case of most compilers, include system calls and memory allocations), it is of course free to elide the copy under the as-if rule, just like for all other optimizations.
Related
This question already has answers here:
What are copy elision and return value optimization?
(5 answers)
Closed 4 years ago.
Today I encountered something I don't really understand about copy constructor.
Consider the next code:
#include <iostream>
using namespace std;
class some_class {
public:
some_class() {
}
some_class(const some_class&) {
cout << "copy!" << endl;
}
some_class call() {
cout << "is called" << endl;
return *this; // <-- should call the copy constructor
}
};
some_class create() {
return some_class();
}
static some_class origin;
static some_class copy = origin; // <-- should call the copy constructor
int main(void)
{
return 0;
}
then the copy constructor is called when assigning origin to copy, which makes sense. However, if I change the declaration of copy into
static some_class copy = some_class();
it isn't called. Even when I am using the create() function, it does not call the copy constructor.
However, when changing it to
static some_class copy = some_class().call();
it does call the copy constructor.
Some research explained that the compiler is allowed to optimize the copy-constructor out, which sound like a good thing. Until the copy-constructor is non-default, as then it might, or might not do something obvious, right?
So when is it allowed for the compiler to optimize out the copy-constructor?
Since C++17, this kind of copy elision is guaranteed, the compiler is not just allowed, but required to omit the copy (or move) construction, even if the copy (or move) constructor has the observable side-effects.
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, as the language rules ensure that no copy/move operation takes place, even conceptually:
In the initialization of a variable, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the
variable type:
T x = T(T(T())); // only one call to default constructor of T, to initialize x
In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:
T f() {
return T();
}
f(); // only one call to default constructor of T
Your code snippet matches these two cases exactly.
Before C++17, the compiler is not required, but allowed to omit the copy (or move) construction, even if the copy/move constructor has observable side-effects. Note that it's an optimization, even copy elision takes place the copy (or move) constructor still must be present and accessible.
On the other hand, call() doesn't match any conditions of copy elision; it's returning *this, which is neither a prvalue nor a object with automatic storage duration, copy construction is required and can't be omitted.
I have been learning about (N)RVO for a last few days. As I read on cppreference in the copy elision article, for C++14 :
... the compilers are permitted, but not required to omit the copy- and move- (since C++11)construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. This is an optimization: even when it takes place and the copy-/move-constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed.
So, either copy or move constructor must be present and accessible. But in the code bellow:
#include <iostream>
class myClass
{
public:
myClass() { std::cout << "Constructor" << std::endl; }
~myClass() { std::cout << "Destructor" << std::endl; }
myClass(myClass const&) { std::cout << "COPY constructor" << std::endl;}
myClass(myClass &&) = delete;
};
myClass foo()
{
return myClass{};
}
int main()
{
myClass m = foo();
return 0;
}
I got the following error: test.cpp: In function 'myClass foo()':
test.cpp:15:17: error: use of deleted function 'myClass::myClass(myClass&&)'
return myClass{};. I get this error even if I don't call foo() from the main(). The same issue with NRVO.
Hence the move constructor is always required, isn't it? (while the copy is not, I checked it)
I do not understand where compiler needs a move constructor. My only guess is that it might be required for constructing a temporary variable, but it sounds doubtful. Do someone knows an answer?
About the compiler: I tried it on g++ and VS compilers, you can check it online: http://rextester.com/HFT30137.
P.S. I know that in C++17 standard the RVO is obligated. But the NRVO isn't, so I want to study out what is going on here to understand when I can use NRVO.
Cited from cppreference:
Deleted functions
If, instead of a function body, the special syntax = delete ; is used, the function is defined as deleted.
...
If the function is overloaded, overload resolution takes place first, and the program is only ill-formed if the deleted function was selected.
It's not the same thing if you explicitly define the move constructor to be deleted. Here because of the presence of this deleted move constructor, although the copy constructor can match, the move constructor is better, thus the move constructor is selected during overload resolution.
If you remove the explicit delete, then the copy constructor will be selected and your program will compile.
Why this:
struct A
{
A(int) {
cout << "construct from int" << endl;
}
A(A&&) = delete;
A(const A &) {
cout << "copy constructor" << endl;
}
};
int main(){
A a = 0;
}
gives me an error:
error: use of deleted function ‘A::A(A&&)’
And why when I add such move constructor
A(A&&) {
cout << "move constructor" << endl;
}
it compiles fine, but programm's output is just
construct from int
So as far as I understand, compiler asks for constructor but doesn't use it. Why? This makes no sense to me.
P.S. I assume that
A a = 0;
is equvalent of
A a = A(0);
but why neither move constructor nor move assignment operator is called?
According to the C++ Standard (12.8 Copying and moving class objects)
31 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.122 This elision of
copy/move operations, called copy elision, is permitted in the
following circumstances (which may be combined to eliminate multiple
copies):
....
— 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
and
30 A program is ill-formed if the copy/move constructor or the
copy/move assignment operator for an object is implicitly odr-used and
the special member function is not accessible (Clause 11). [ Note:
Copying/moving one object into another using the copy/move constructor
or the copy/move assignment operator does not change the layout or
size of either object. —end note ]
My copy constructor is not being called and I'm not sure why. Here's my code:
template <typename T>
class SmartPtr
{
public:
explicit SmartPtr(T *p) : m_p(p) { cout << "ctor" << endl; }
SmartPtr(const SmartPtr& p) : m_p(p.m_p) { cout << "copy ctor" << endl;}
private:
T* m_p;
};
int main()
{
SmartPtr<int> pt4 = SmartPtr<int>(new int);
}
The output is only "ctor". It looks like a default copy constructor is used. If I add "explicit" then it doesn't compile, giving the error:
"error: no matching function for call to ‘SmartPtr<int>::SmartPtr(SmartPtr<int>)’"
What am I doing wrong here?
This is what is known as Copy Elision. It's a nice optimization where a copy clearly isn't necessary. Instead of effectively running the code:
SmartPtr<int> __tmp(new int);
SmartPtr<int> ptr4(__tmp);
__tmp.~SmartPtr<int>();
The compiler can know that __tmp only exists to construct ptr4, and thus is allowed to construct __tmp in-place in the memory owned by ptr4 as if the actual code originally run was just:
SmartPtr<int> ptr4(new int);
Note that you can tell the compiler NOT to do this too. For instance, on gcc, you can pass the -fno-elide-constructors option and with that single change (additionally logging the destructor), now your code prints:
ctor
copy ctor // not elided!
dtor
dtor // extra SmartPtr!
See demo.
In the standard, §12.8:
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 ...
In a throw-expression, when ...
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 the exception-declaration of an exception handler (Clause 15) ...
[Example:
class Thing {
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f() {
Thing t;
return t;
}
Thing t2 = f();
Here the criteria for elision can be combined to eliminate two calls to the copy constructor of class Thing:
the copying of the local automatic object t into the temporary object for the return value of function f()
and the copying of that temporary object into object t2. Effectively, the construction of the local object t
can be viewed as directly initializing the global object t2, and that object’s destruction will occur at program
exit. Adding a move constructor to Thing has the same effect, but it is the move construction from the
temporary object to t2 that is elided. —end example ]
So I have a question for you. :)
Can you tell me the output the following code should produce?
#include <iostream>
struct Optimized
{
Optimized() { std::cout << "ctor" << std::endl; }
~Optimized() { std::cout << "dtor" << std::endl; }
Optimized(const Optimized& copy) { std::cout << "copy ctor" << std::endl; }
Optimized(Optimized&& move) { std::cout << "move ctor" << std::endl; }
const Optimized& operator=(const Optimized& rhs) { std::cout << "assignment operator" << std::endl; return *this; }
Optimized& operator=(Optimized&& lhs) { std::cout << "move assignment operator" << std::endl; return *this; }
};
Optimized TestFunction()
{
Optimized a;
Optimized b = a;
return b;
}
int main(int argc, char* argv[])
{
Optimized test = TestFunction();
return 0;
}
My first response would be:
ctor
copy ctor
move ctor
dtor
dtor
dtor
and it IS true, but only if compiler optimization is turned off. When optimization is turned ON then the output is entirely different. With optimization turned on, the output is:
ctor
copy ctor
dtor
dtor
With compiler optimization, the test variable is the return variable.
My question is, what conditions would cause this to not be optimized this way?
I have always been taught that returning a struct/class which results in extra copy constructors could better be optimized by being passed in as a reference but the compiler is doing that for me. So is return a structure still considered bad form?
This is known as Copy Elision and is a special handling instead of copying/moving.
The optimization is specifically allowed by the Standard, as long as it would be possible to copy/move (ie, the method is declared and accessible).
The implementation in a compiler is generally referred to, in this case, as Return Value Optimization. There are two variations:
RVO: when you return a temporary (return "aa" + someString;)
NRVO: N for Named, when you return an object that has a name
Both are implemented by major compilers, but the latter may kick in only at higher optimization levels as it is more difficult to detect.
Therefore, to answer your question about returning structs: I would recommend it. Consider:
// Bad
Foo foo;
bar(foo);
-- foo can be modified here
// Good
Foo const foo = bar();
The latter is not only clearer, it also allows const enforcement!
Both outputs are permissible. The C++03 language standard says, in clause 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
The output this code will produce is unpredictable, since the language specification explicitly allows optional elimination (elision) of "unnecessary" temporary copies of class objects even if their copy constructors have side effects.
Whether this will happen or not might depend on may factors, including the compiler optimization settings.
In my opinion calling the above copy elision an "optimization" is not entirely correct (although the desire to use this term here is perfectly understandable and it is widely used for this purpose). I'd say that the term optimization should be reserved to situations when the compiler deviates from the behavior of the abstract C++ machine while preserving the observable behavior of the program. In other words, true optimization implies violation of the abstract requirements of the language specification. Since in this case there's no violation (the copy elision is explicitly allowed by the standard), there's no real "optimization". What we observe here is just how the C++ language works at its abstract level. No need to involve the concept of "optimization" at all.
Even when passing back by value the compiler can optimise the extra copy away using Return Value Optimisation see; http://en.wikipedia.org/wiki/Return_value_optimization