Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does C++ allow std::initializer_list to be coerced to primitive types, and be used to initialise them?

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.

like image 613
zhiayang Avatar asked Aug 08 '15 16:08

zhiayang


2 Answers

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 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 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 arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then:

(1.3) — for some argument j, ICSj(F1) is a better conversion sequence than ICSj(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.

like image 85
Piotr Skotnicki Avatar answered Sep 18 '22 08:09

Piotr Skotnicki


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.

like image 39
Puppy Avatar answered Sep 17 '22 08:09

Puppy