What changes to C++ made copy initialization work for class with explicit constructor?

后端 未结 2 1413
余生分开走
余生分开走 2020-12-30 02:02

Consider this code:

struct X{
    explicit X(){}
    explicit X(const X&){}
};

void foo(X a = X()){}

int main(){}

Using C++14 standar

相关标签:
2条回答
  • 2020-12-30 02:32

    The most important for the example in the question rule is [expr.type.conv]/2. But lets start from [dcl.init]/17:

    The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.

    ...

    (17.6) — If the destination type is a (possibly cv-qualified) class type:

    — If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [Example: T x = T(T(T())); calls the T default constructor to initialize x.  — end example]

    So, in X a = X(), the initializer expression X() is used to initialize the destination object. Of course, this is not enough to answer: why default constructor is selected (i.e. how X() becomes ()) and why explicit default constructor is fine.

    The X() expression is explicit type conversion in functional notation, so lets look into [expr.type.conv]/2:

    If the initializer is a parenthesized single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression. If the type is cv void and the initializer is (), the expression is a prvalue of the specified type that performs no initialization. Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized with the initializer.

    Emphasis of the relevant sentence is mine. It says that for X():

    • the object is initialized with () (it is "the initializer" by [expr.type.conv]/1), that's why the default constructor is selected;

    • the object is direct-initialized, that's why it is OK that the default constructor is explicit.


    In more details: when the initializer is (), [dcl.init]/(17.4) apply:

    If the initializer is (), the object is value-initialized.

    [dcl.init]/8:

    To value-initialize an object of type T means:
    — if T is a (possibly cv-qualified) class type with either no default constructor ([class.ctor]) or a default constructor that is user-provided or deleted, then the object is default-initialized;

    [dcl.init]/7:

    To default-initialize an object of type T means:
    — If T is a (possibly cv-qualified) class type, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one for the initializer () is chosen through overload resolution. The constructor thus selected is called, with an empty argument list, to initialize the object.

    [over.match.ctor]/1

    When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized.


    In C++14, [dcl.init](17.6) was saying:

    — If the destination type is a (possibly cv-qualified) class type:

    — 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. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution ([over.match]).

    So for X a = X(), only converting (non-explicit) constructors accepting one argument of type X will be considered (which are copy and move constructors).

    0 讨论(0)
  • 2020-12-30 02:33

    Because the behavior of copy elision changes from C++17; for this case copy elision is mandatory.

    Mandatory elision of copy/move operations

    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:

      T f() {
          return T();
      }
      
      T x = T(T(f())); // only one call to default constructor of T, to initialize x
      

    Note: the rule above does not specify an optimization: C++17 core language specification of prvalues and temporaries is fundamentally different from that of the earlier C++ revisions: there is no longer a temporary to copy/move from. Another way to describe C++17 mechanics is "unmaterialized value passing": prvalues are returned and used without ever materializing a temporary.

    And for copy initialization:

    The effects of copy initialization are:

    • First, if T is a class type and the initializer is a prvalue expression whose cv-unqualified type is the same class as T, the initializer expression itself, rather that a temporary materialized from it, is used to initialize the destination object: see copy elision (since C++17)

    • If T is a class type and the cv-unqualified version of the type of other is T or a class derived from T, the non-explicit constructors of T are examined and the best match is selected by overload resolution. The constructor is then called to initialize the object.

    That means for X a = X(), a will be default constructed directly, the copy/move constructors and their side effects will be omiited completely. The selection of non-explicit constructors for overload resolution won't take place, which is required in C++14 (and before). For these guaranteed cases, the copy/move constructors don't participate in, then it won't matter whether they're explicit or not.

    0 讨论(0)
提交回复
热议问题