问题
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?
回答1:
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].
回答2:
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.
来源:https://stackoverflow.com/questions/48617690/direct-initialization-with-empty-initializer-list