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).
struct A { int x; } a = { 0 };
also employs copy-initialization ofa
from{ 0 }
, yet there is no copy being made. – Magreeprvalue
and temporary materialization brouhaha. – Dobsonfly