Why is the std::initializer_list constructor preferred when using a braced initializer list?
§13.3.1.7 [over.match.list]/p1:
When objects of non-aggregate class type
T
are list-initialized
(8.5.4), overload resolution selects the constructor in two phases:
- Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class
T
and the argument list consists of
the initializer list as a single argument.- If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all
the constructors of the classT
and the argument list consists of
the elements of the initializer list.
If the initializer list has no elements and
T
has a default
constructor, the first phase is omitted. In copy-list-initialization,
if anexplicit
constructor is chosen, the initialization is
ill-formed.
As long as there is a viable initializer-list constructor, it will trump all non-initializer-list constructors when list-initialization is used and the initializer list has at least one element.
Why does std::initializer_list in ctor not behave as expected?
Meyers is mostly correct (the exception is that T{}
is value-initialization if a default constructor exists), but his statement is about overload resolution. That takes place after CTAD, which chooses the class (and hence the set of constructors) to use.
CTAD doesn’t “prefer” initializer-list constructors in that it prefers copying to wrapping for nestable templates like std::vector
or std::optional
. (It’s possible to override this with deduction guides, but the standard library uses the default, as one might expect.) This makes some sense in that it prevents creating strange types like std::optional<std::optional<int>>
, but it makes generic code harder to write because it gives
template<class T> void f(T x) {
std::vector v{x};
// …
}
a meaning that depends on the type of its argument in an irregular and non-injective fashion. In particular, v
might be std::vector<int>
with T
=int
or with T
=std::vector<int>
, despite being std::vector<std::deque<int>>
if T
=std::deque<int>
. It’s unfortunate that a tool for computing one type based on some others is not usable in a generic context.
Constructors taking initializer lists
It sounds like you're asking for motivation, as opposed to where in the standard it says this must be done. For that you can look at the original proposal N1919 for intializer lists by Bjarne Stroustrup, the C++ language creator.
He lists four ways to intialise an object:
X t1 = v; // “copy initialization” possibly copy construction
X t2(v); // direct initialization
X t3 = { v }; // initialize using initializer list
X t4 = X(v); // make an X from v and copy it to t4
Note that he's not talking about C++11, or a proposed version where initializer lists were introduced. This is back in C++98. That brace initialiser syntax already worked, but only for C-style structs i.e. with no user-defined constructor. This is a holdover from C, which has always allowed initialising structs (and arrays) this way, and it will always do the same thing: initialise element-by-element.
The whole point of the proposal is to allow initialisation for proper C++ objects like std::vector<int>
in the same way as those C-style structs and arrays: C++ is meant to allow user-defined classes to look like built-in types (hence e.g. operator overloading) and here's one place it didn't. To turn your question around, the thing that is odd is not that std::vector<int>{3}
calls the initialiser list constructor, the thing that is odd is that std::vector<std::string>{3}
calls the non-initialiser list constructor. Why does it ever call a non-initialiser list constructor? That's not really what brace initialisation was originally for. The answer to that is to allow fix-length containers with hand-written constructors, like this:
class Vector3D {
public:
Vector3D(double x, double y, double z) { /*...*/ }
// ...
};
Vector3D v = {1, 2, 3}; // Ought to call non-initialiser list constructor
That is why constructors taking std::initializer_list
are preferred (if available) when using brace initialisation. For those knowing the background, the use of brace initialisers for everything, as has become the fashion, seems really perverse: Foo f{7}
looks like f
will directly contain the number 7
and nothing else after construction completes, not that it does something arbitrary like construct something 7 elements long.
List initialization (aka uniform initialization) and initializer_list?
List-initialization prefers constructors with a std::initializer_list
argument. From cppreference:
The effects of list-initialization of an object of type T are:
[...cases that do not apply here ...]
- Otherwise, the constructors of T are considered, in two phases:
All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have
default values, are examined, and matched by overload resolution
against a single argument of type std::initializer_listIf the previous stage does not produce a match, all constructors of T participate in overload resolution against the set of arguments
that consists of the elements of the braced-init-list, with the
restriction that only non-narrowing conversions are allowed. If this
stage produces an explicit constructor as the best match for a
copy-list-initialization, compilation fails (note, in simple
copy-initialization, explicit constructors are not considered at all).
While direct initialization does not prefer the initializer_list constructor. It calls the constructor that takes the size as argument.
And from https://en.cppreference.com/w/cpp/container/vector/vector:
Note that the presence of list-initializing constructor (10) means
list initialization and direct initialization do different things:std::vector<int> b{3}; // creates a 1-element vector holding {3}
std::vector<int> a(3); // creates a 3-element vector holding {0, 0, 0}
std::vector<int> d{1, 2}; // creates a 2-element vector holding {1, 2}
std::vector<int> c(1, 2); // creates a 1-element vector holding {2}
And how can I call std::vector::vector(size_t) with list-initialization?
As explained above, the presence of the std::initializer_list
constructor prevents you from calling the other constructor via list-initialization.
How does nested list-initialization forward its arguments?
Usually, expressions are analyzed inside-out: The inner expressions have types and these types then decide which meaning the outer operators have and which functions are to be called.
But initializer lists are not expressions, and have no type. Therefore, inside-out does not work. Special overload resolution rules are needed to account for initializer lists.
The first rule is: If there are constructors with a single parameter that is some initializer_list<T>
, then in a first round of overload resolution only such constructors are considered (over.match.list).
The second rule is: For each initializer_list<T>
candidate (there could be more than one of them per class, with different T
each), it is checked that each initializer can be converted to T
, and only those candidates remain where this works out (over.ics.list).
This second rule is basically, where the initializer-lists-have-no-type hurdle is taken and inside-out analysis is resumed.
Once overload resolution has decided that a particular initializer_list<T>
constructor should be used, copy-initialization is used to initialize the elements of type T
of the initializer list.
Why does C++ allow std::initializer_list to be coerced to primitive types, and be used to initialise them?
That {}
syntax is a braced-init-list, and since it is used as an argument in a function call, it copy-list-initializes a corresponding parameter.
§ 8.5 [dcl.init]/p17:
(17.1) — If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).
§ 8.5.4 [dcl.init.list]/p1:
List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is
called an initializer list, and the comma-separated initializer-clauses of the list are called the elements of the
initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; [...]
For a class-type parameter, with list-initialization, overload resolution looks up for a viable constructor in two phases:
§ 13.3.1.7 [over.match.list]/p1:
When objects of non-aggregate class type
T
are list-initialized (8.5.4), overload resolution selects the constructor
in two phases:— Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class
T
and the argument list consists of the initializer list as a single argument.— If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class
T
and the argument list consists of the elements of the initializer list.
but:
If the initializer list has no elements and
T
has a default constructor, the first phase is omitted.
Since std::deque<T>
defines a non-explicit default constructor, one is added to a set of viable functions for overload resolution. Initialization through a constructor is classified as a user-defined conversion (§ 13.3.3.1.5 [over.ics.list]/p4):
Otherwise, if the parameter is a non-aggregate class
X
and overload resolution per 13.3.1.7 chooses a single
best constructor ofX
to perform the initialization of an object of typeX
from the argument initializer list,
the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion
sequence an identity conversion.
Going further, an empty braced-init-list can value-initialize its corresponding parameter (§ 8.5.4 [dcl.init.list]/p3), which for literal types stands for zero-initialization:
(3.7) — Otherwise, if the initializer list has no elements, the object is value-initialized.
This, for literal types like bool
, doesn't require any conversion and is classified as a standard conversion (§ 13.3.3.1.5 [over.ics.list]/p7):
Otherwise, if the parameter type is not a class:
(7.2) — if the initializer list has no elements, the implicit conversion sequence is the identity conversion.
[ Example:
void f(int);
f( { } );
// OK: identity conversion
— end example ]
Overload resolution checks in first place if there exists an argument for which a conversion sequence to a corresponding parameter is better than in another overload (§ 13.3.3 [over.match.best]/p1):
[...] Given these definitions, a viable function
F1
is defined to be a better function than another viable function
F2
if for all argumentsi
,ICSi(F1)
is not a worse conversion sequence thanICSi(F2)
, and then:(1.3) — for some argument
j
,ICSj(F1)
is a better conversion sequence thanICSj(F2)
, or, if not that, [...]
Conversion sequences are ranked as per § 13.3.3.2 [over.ics.rank]/p2:
When comparing the basic forms of implicit conversion sequences (as defined in 13.3.3.1)
(2.1) — a standard conversion sequence (13.3.3.1.1) is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and [...]
As such, the first overload with bool
initialized with {}
is considered as a better match.
Related Topics
C++ Polymorphism Without Pointers
What Is an Iterator in General
Boost::Asio - How to Interrupt a Blocked Tcp Server Thread
Understanding the Dangers of Sprintf(...)
Compile-Time or Runtime Detection Within a Constexpr Function
Conflict Between Dynamic Linking Priority in Osx
Subtle C++ Inheritance Error with Protected Fields
Is Sizeof(*Ptr) Undefined Behavior When Pointing to Invalid Memory
G++ How to Get Warning on Ignoring Function Return Value
Valgrind Memory Leak Errors When Using Pthread_Create
Avoiding Denormal Values in C++
Is There Any Way a C/C++ Program Can Crash Before Main()
Opencv Unable to Set Up Svm Parameters
Is Left-Shifting (<<) a Negative Integer Undefined Behavior in C++11
Function Composition in C++/C++11
Visual Studio 2015 Run-Time Dependencies or How to Get Rid of Universal Crt