Saturday, February 21, 2015

Why auto deduces std::initializer_list for a braced initializer

For some years, I've wondered aloud why
auto x { 1, 2, 3 };
deduces x's type as std::initializer_list, even though the braced initializer itself ("{1, 2, 3}") has no type. One example of my out-loud wondering took place on this blog about a year ago ("If braced initializers have no type, why is the committee so insistent on deducing one for them?"). Another was during my talk at CppCon 2014, "Type deduction and why you care". James Hopkin recently watched the video of that presentation, and he then sent me the following quite interesting message:
You ask at one point for any info on the motivation for N3922's special case for auto. I did a bit of digging and thought I'd send you what I found.

The short story is that N2640 proposed the special case that auto should deduce braced initializers as initializer_lists, without realizing that doing so broke uniform initialization (e.g. it makes int x{7}; and auto x{7}; very different). N3922 fixes that by (of course!) introducing another special case: single parameter braced initializers have their own rule.

Slightly more detail: N2640 tries to keep template argument deduction simple but attempts to allow a braced initializer to be passed to two or more functions via assigning it to an auto. This got turned into wording in N2672. Note that Stroustrup's previous design in N2532 allows deduction of initializer_lists for both unconstrained template parameters and auto, which is more consistent but also breaks uniform initialization.

None of this explains why N3922 didn't just remove the special case for auto. That wouldn't have resulted in silent changes to the meaning of code and would have simplified the language.
For purposes of understanding why there's a special rule for auto type deduction and braced initializers, the key sentence in N2640 is this:
On the other hand, being able to deduce an initializer_list for T is attractive to
auto x = { 1, 1, 2, 3, 5 };
which was deemed desirable behavior since the very beginning of the EWG discussions about initializer lists.
That's not a lot of detail, but it's more than I've ever seen before. Thank you, James Hopkin, for digging up this background information!



Arne Mertz said...

I have just written a blog post on the topic, after I had watched the video from CppCon:

In short, I consider local variables that are initializer_lists a corner case that should be documented in code by explicitly mentioning the type:

auto il = std::initializer_list{ 1, 2, 3 };

For any other usage than the seemingly artificial case in N2640, both N2640 and N3922 have brought problems that are not justifiable by the benefits for such corner cases.

The rules for direct initialization in N3922 make sense, because they do what would be expected for uniform initialization. However, usind copy initialization to deduce initializer_list is an inconsistency in the language.

Maybe a generator function รก la make_tuple is in place here, together with an overhaul of N3922:

auto a{ 42 }; //OK, int -> N3922
auto b{ 1, 2 }; //ERROR -> N3922
auto c = { 3.14 }; //ERROR -> new!
auto d = { 3, 5 }; //ERROR -> new!
//new function:
auto e = std::make_initializer_list(1, 2, 3, 5, 8); //initializer_list

Unknown said...

Perhaps another reason that originally led the Committee to have auto deduce std::initializer_list (although it's arguable whether this is a valid reason) was to support braced lists in range-based for loops, where one could write:

for (auto x : {1, 2, 3}) { ... }

Since range-based for is defined to be equivalent to:

auto && __range = range-init;
for (...)

The loop above would have not been legal if auto were not able to deduce anything given a braced-init-list. And if it has to deduce something, then std::initializer_list is probably the most natural choice.

Now I do realize that the code in your blog post uses direct-initialization, while range-based for is defined in terms of copy-initialization, but the Committee probably thought that the two should not behave differently in terms of type deduction.

Matt said...

Out of curiosity -- I'm wondering, what would be the potential downsides of relaxing the following: "the braced initializer itself ("{1, 2, 3}") has no type"?

Scott Meyers said...

@Andy Prowl: Note that the only reason range-based for loops currently work with braced initializers is that there's a special provision in the spec for them (per 6.5.4/1). That special provision is necessary, because a braced initializer isn't an expression. The current special treatment says to consider the braced initializer as an expression (even though it's not), then, as you point out, the spec relies on the special auto type deduction rule. But the spec could just as easily have defined the semantics of a range-based for with a braced initializer directly, i.e., without the semantic circumlocution through auto.

Scott Meyers said...

@Matt: I suspect that trying to give braced initializers a type would lead to all kinds of trouble. Consider:

struct Wacko {
int x;
double d;
bool b;

Wacko w { 10, 5, 0 };

What is the type of the braced initializer used to initialize w?

Currently, braced initializers are simply a syntactic construct, and their semantics is dependent on context. I don't think that's a problem that needs to be solved. I think the problem--or at least a problem--is the special treatment accorded braced initializers when auto enters the picture, a problem that I frankly think N3922 makes worse.

Casey said...

"None of this explains why N3922 didn't just remove the special case for auto." That's exactly what the earlier paper, N3861, proposed. Ville submitted it as National Body comment FI-3, but CWG rejected the change. There's some discussion that hints at CWG's reasoning in N3912

Casey said...

It may be easier to get support in the committee for removing the special case allowing auto to deduce an initializer_list once the Concepts PDTS N4377 is incorporated into the standard. Concepts expands the semantics for type deduction involving placeholders such that, e.g.,

std::initializer_list foo = {1,2,3};

will deduce std::initializer_list. This could replace the use case for

auto foo = {1,2,3};

Maybe CWG will agree that a small backwards-compatibility break for a niche use case is a reasonable cost to pay for removing some of the hard to teach and learn special cases from the type deduction rules.

Casey said...

*sigh* That should read

std::initializer_list<auto> foo = {1,2,3};


Vadim Petrochenkov said...

>It may be easier to get support in the committee for removing the special case allowing auto to deduce an initializer_list once the Concepts PDTS N4377 is incorporated into the standard.

Sadly, it is likely not going to happen.
Here's a comment on this exact issue from Ville Voutilainen.

Unknown said...

I do agree with Casey in this regard.
C++ Urdu Tutorial

KyleJStrand said...

Yeah, I'm really not thrilled with the brace-initializer-list situation. Even improving the `auto` situation won't be a complete fix, since overloaded constructors can behave in confusing ways (as I think you point out in a later Item from EMC++). For instance, see this StackOverflow question, where the only answer recommends using a Qt feature that disables generation of constructors using initializer lists:

Coming from Python, I would think that the natural solution would be to assume by default that things enclosed in braces are not initializer-lists unless there is no other valid interpretation, but to allow explicitly forcing them to be interpreted that way using a trailing comma, e.g.:

auto i_list = {1,2,3,};

Scott Meyers said...

@KyleJStrand: I increasingly think that auto shouldn't ever deduce a std::initializer_list from a braced initializer, though the Standardization Committee disagrees with me. (They took a vote on that idea and rejected it.) As for your trailing comma idea, I view that as too subtle.

To be honest, I just don't think it's that much work to type "std::initializer_list<WhateverType>" instead of "auto" in what I expect to be the comparatively rare cases where a std::initializer_list object is what you want. Warping the language to avoid that "extra typing" doesn't strike me as a feature that carries its weight.

KyleJStrand said...

@ScottMeyers I think I'm with you, really--even if my "trailing comma" idea were implemented, I don't really think I'd ever use it. It would just be an acceptable way to give the people in favor of including the capability to deduce initializer-lists as types what they want, while allowing me to avoid getting that behavior everywhere it doesn't make sense.