Why is a public copy constructor required even if

2019-06-16 11:49发布

问题:

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;
}

回答1:

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



回答2:

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.



回答3:

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.



回答4:

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.