struct X
{
X() { std::cout << "default ctor" << std::endl; }
};
int main()
{
X({});
}
This prints out
default ctor
and that makes sense because empty brace value-initializes the object (I think). However,
struct X
{
X() { std::cout << "default ctor" << std::endl; }
X(std::initializer_list<int>) { std::cout << "initializer list" << std::endl; }
};
int main()
{
X({});
}
For this, I got
initializer list
I don't find this behavior so strange, but I'm not fully convinced. What is the rule for this?
Is this behavior written in some part of the standard?
Direct-initialization is more permissive than copy-initialization: copy-initialization only considers non-explicit constructors and non-explicit user-defined conversion functions, while direct-initialization considers all constructors and all user-defined conversion functions.
Initializer List is used in initializing the data members of a class. The list of members to be initialized is indicated with constructor as a comma-separated list followed by a colon. Following is an example that uses the initializer list to initialize x and y of Point class.
The initializer list is used to directly initialize data members of a class. An initializer list starts after the constructor name and its parameters.
There are two ways to initialize a class object: Using a parenthesized expression list. The compiler calls the constructor of the class using this list as the constructor's argument list. Using a single initialization value and the = operator.
To see what's really going on, declare copy and move constructors, compile in C++14 mode or earlier, and disable copy elision.
Coliru link
Output:
default ctor
move ctor
In the first snippet, the compiler looks for constructors of X
that take a single argument, since you've provided a single argument. These are the copy and move constructor, X::X(const X&)
and X::X(X&&)
, which the compiler will implicitly declare for you if you do not declare them yourself. The compiler then converts {}
to an X
object using the default constructor, and passes that X
object to the move constructor. (You must use fno-elide-constructors
to see this otherwise the compiler will elide the move, and in C++17 copy elision became mandatory.)
In the second snippet, the compiler now has a choice of converting {}
to X
(then calling the move constructor), or converting {}
to std::initializer_list<int>
(then calling the initializer list constructor). According to [over.ics.list]/6.2, the conversion from {}
to X
, which calls the default constructor, is a user-defined conversion, while according to [over.ics.list]/4, the conversion from {}
to std::initializer_list<int>
is the identity conversion. The identity conversion is better than a user-defined conversion, so the compiler calls the initializer list constructor.
Is this behavior written in some part of the standard?
Of course. It's all dictated by the rules in [dcl.init]/16, emphasis mine to match your initializer:
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.
If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized ([dcl.init.list]).
[...]
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]). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.
- [...]
You supply a parenthesized empty brace-init-list, so only the later bullet applies. Constructors are considered, and in the first case we end up doing a copy-initialization from a default initialized X
. In the latter case, the initializer_list
c'tor is chosen as a better match. The rule for choosing this overload is specified in [over.ics.list]:
When an argument is an initializer list ([dcl.init.list]), it is not an expression and special rules apply for converting it to a parameter type.
If the parameter type is std::initializer_list or “array of X” and all the elements of the initializer list can be implicitly converted to X, the implicit conversion sequence is the worst conversion necessary to convert an element of the list to X. This conversion can be a user-defined conversion even in the context of a call to an initializer-list constructor.
Otherwise, if the parameter is a non-aggregate class X and overload resolution per [over.match.list] chooses a single best constructor of X to perform the initialization of an object of type X from the argument initializer list, the implicit conversion sequence is a user-defined conversion sequence. If multiple constructors are viable but none is better than the others, the implicit conversion sequence is the ambiguous conversion sequence. User-defined conversions are allowed for conversion of the initializer list elements to the constructor parameter types except as noted in [over.best.ics].
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With