Having a public copy constructor will make the little program
compile, but not showing the side effect "Copy".
#include <iostream>
class X
{
public:
X(int) { std::cout << "Construct" << std::endl; }
// Having a public copy constructor will make the little program
// compile, but not showing the side effect "Copy".
private:
X(const X&) { std::cout << "Copy" << std::endl; }
private:
X& operator = (const X&);
};
int main() {
X x = 1;
return 0;
}
Here are the relevant bits of the C++ standard that are involved:
[dcl.init]/16, bullet 6, sub-bullet 1: If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered.... If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed. [emphasis added]
In other words, it doesn't matter if a compiler optimization could elide the copy, the initialization is ill-formed because there are no applicable constructors. Of course, once you make the copy constuctor public, the following section applies:
[class.copy]/31: 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.... This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
bullet 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
You have used so-called "copy initialization" (defined in [decl.init]
). The defined meaning is to construct a temporary object of type X
using the int
constructor, and then initialize x
from the temporary using the copy constructor.
However, the standard also permits an optimization called "copy constructor elision" (defined in [class.copy]
) in this case. If that optimization is applied there is no temporary. x
is constructed using the int
constructor just as if you'd written so-called "direct initialization" X x(1);
.
In order that you don't accidentally write code that compiles when the optimization is applied but not when it isn't, the standard requires that the copy constructor must be accessible even if it is elided. Hence, the constructor must be public even though (with the compiler and options you're using) it's not called.
In C++11 move constructors are considered, and are eligible for elision too. However this class X
doesn't have a move constructor, so C++11 and C++03 are the same for this example.
You initialisation is being optimised by the compiler down to:
X x(1)
This is a kind of copy elision, and is allowed by the standard, even though it can remove side effects like you saw.
From the C++03 standard section 12.8:
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
implemen-tation 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 areturnstatement 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 tempo-rary object
directly into the target of the omitted copy
The second case is what we have here.
My best guess is that this is a compiler optimization. There is a valid path to declaring an object this way if you have a copy constructor. Consider doing this explicitly:
int main() {
X x(X(1));
return 0;
}
Or more explicitly:
int main() {
X intermediate(1);
X x(intermediate);
return 0;
}
So instead of using operator=
, it knows you are trying to initialize your object since the declaration. In essence, the compiler is being "smart". Finally, it optimizes this again to this step:
int main() {
X x(1);
return 0;
}
Therefore, after the compiler figures out "what you were trying to do" this becomes a standard initialization of the object.
EDIT
For completeness, notice that if you try this:
int main() {
X x = 1;
x = 2;
return 0;
}
You will see the issue with private operator=
. Explicitly, this is important to notice that operator=
is never actually used in the original initialization question above even though =
appears in the code.