This question is regarding std::initializer_list, and why it is allowed to initialise primitive types. Consider the following two functions:
void foo(std::string arg1, bool arg2 = false);
void foo(std::string arg1, std::deque<std::string> arg2, bool arg3 = false);
Why is it that, when calling foo like this:
foo("some string", { });
The first overload is picked, instead of the second? Well, actually not why it's picked, it's because { }
can be used to initialise anything, including primitive types. My question is the reasoning behind this.
std::initializer_list takes { args... }
, and as such cannot have indeterminate length at the time of compilation. Attempting to do something like bool b = { true, true }
gives error: scalar object 'b' requires one element in initialiser
.
While it might have seemed like a good idea to allow uniform initialisation, the fact is that this is confusing and entirely unexpected behaviour. Indeed, how is the compiler able to do this, without some magic in the background doing std::initializer_list things?
Unless { args... }
is a C++ lexical construct, in which case my point still stands: why is it allowed to be used in the initialisation of primitive types?
Thanks. I had quite the bug-hunting session here, before realising that the wrong overload was being called. Spent 10 minutes figuring out why.
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 functionF2
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.
Unfortunately, {}
does not actually indicate an std::initializer_list
. It is also used for uniform initialization. Uniform initialization was intended to fix the problems of the piles of different ways C++ objects could be initialized but ended up just making things worse, and the syntactic conflict with std::initializer_list
is fairly awful.
Bottom line is that {}
to denote an std::initializer_list
and {}
to denote uniform initialization are two different things, except when they're not.
Indeed, how is the compiler able to do this, without some magic in the background doing std::initialiser_list things?
The aforementioned magic most assuredly exists. { args... }
is simply a lexical construct and the semantic interpretation depends on context- it is certainly not an std::initializer_list
, unless the context says it is.
why is it allowed to be used in the initialisation of primitive types?
Because the Standards Committee did not properly consider how broken it was to use the same syntax for both features.
Ultimately, uniform init is broken by design, and should realistically be banned.
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