Why Do Auto and Template Type Deduction Differ for Braced Initializers

What is the difference between auto deduction and template type deduction?

auto type deduction is usually the same as template type deduction, but auto
type deduction assumes that a braced initializer represents a std::initializer_list, and template type deduction doesn’t.

When an auto–declared variable is initialized with a
braced initializer, the deduced type is an instantiation of std::initializer_list.
But if the corresponding template is passed the same initializer, type deduction fails,
and the code is rejected:

auto x = { 11, 23, 9 }; // x's type is     
//std::initializer_list<int>
template<typename T> // template with parameter
void f(T param); // template with parameter

However, if you specify in the template that param is a std::initializer_list<T>
for some unknown T, template type deduction will deduce what T is:

template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); // T deduced as int, and initList's
// type is std::initializer_list<int>

Remember


  • auto type deduction is usually the same as template type deduction, but auto type deduction assumes that a braced initializer represents a
    std::initializer_list, and template type deduction doesn’t.

Why is there a special type deduction rule for auto and braced initializers in C++11/C++14?

The rationale is in N2640, which wanted to ban deduction of a plain type parameter from a braced initializer list in general:

template<class T>
void inc(T, int); // (1)

template<class T>
void inc(std::initializer_list<T>, long); // (2)

inc({1, 2, 3}, 3); // Calls (2). (If deduction had succeeded
// for (1), (1) would have been called — a
// surprise.)

But carved out a special exception for auto:

On the other hand, being able to deduce an initializer_list<X> for
T is attractive to allow:

auto x = { 1, 1, 2, 3, 5 };
f(x);
g(x);

which was deemed desirable behavior since the very beginning of the
EWG discussions about initializer lists. Rather than coming up with a
clever deduction rule for a parameter type T matched with a {}-list
(an option we pursued in earlier sketches and drafts of this paper),
we now prefer to handle this with a special case for "auto" variable
deduction when the initializer is a {}-list. I.e., for the specific
case of a variable declared with an "auto" type specifier and a
{}-list initializer, the "auto" is deduced as for a function
f(initializer_list<T>) instead of as for a function f(T).

Why is there a special type deduction rule for auto and braced initializers in C++11/C++14?

The rationale is in N2640, which wanted to ban deduction of a plain type parameter from a braced initializer list in general:

template<class T>
void inc(T, int); // (1)

template<class T>
void inc(std::initializer_list<T>, long); // (2)

inc({1, 2, 3}, 3); // Calls (2). (If deduction had succeeded
// for (1), (1) would have been called — a
// surprise.)

But carved out a special exception for auto:

On the other hand, being able to deduce an initializer_list<X> for
T is attractive to allow:

auto x = { 1, 1, 2, 3, 5 };
f(x);
g(x);

which was deemed desirable behavior since the very beginning of the
EWG discussions about initializer lists. Rather than coming up with a
clever deduction rule for a parameter type T matched with a {}-list
(an option we pursued in earlier sketches and drafts of this paper),
we now prefer to handle this with a special case for "auto" variable
deduction when the initializer is a {}-list. I.e., for the specific
case of a variable declared with an "auto" type specifier and a
{}-list initializer, the "auto" is deduced as for a function
f(initializer_list<T>) instead of as for a function f(T).

return and auto deduce std::initializer_list

Well, because the Standard says so, and because a braced-init-list is not an expression. Per paragraph 5.1.2/4 of the C++11 Standard:

[...] If
a lambda-expression does not include a trailing-return-type, it is as if the trailing-return-type denotes the
following type:

— if the compound-statement is of the form

{ attribute-specifier-seq(opt) return expression ; }

the type of the returned expression after lvalue-to-rvalue conversion (4.1), array-to-pointer conversion
(4.2), and function-to-pointer conversion (4.3);

— otherwise, void.

The above makes it clear that the return type will be deduced to be anything else then void if and only if the return statement is followed by an expression, and a braced-init-list is not in itself an expression - it does not have a type, and it does not yield a value. It is just a language construct that can be used in the context of initialization.

The above paragraph also provides an example:

[ Example:

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return { 1, 2 }; }; // error: the return type is void (a
// braced-init-list is not an expression)

end example ]

Finally, if the question is:

"Why a special rule was introduced for deducing the type of an auto variable initialized from a braced-init-list, while a similar rule was not introduced for deducing the return type of a lambda when return is followed by a braced-init-list?"

Then the question is not constructive. Also notice, that type deduction for templates does not work with braced-init-lists either:

template<typename T>
void foo(T);

foo({1, 2}); // ERROR! T is NOT deduced to be std::initializer_list<int>

Using auto to deduce the type of a nested initializer list

I have a program where these initializer lists could be of arbitrary sizes and depths so hardcoding the types is not practical.

Then you need to fix that problem.

You should not think of braced-init-lists as a quick-and-dirty way of making arrays of values without having to think about their types. That's not their purpose. Their purpose is to initialize values. The type std::initializer_list is intended to be an intermediary phase in the process of initializing some type (which is why constructors that take a single initializer_list are given special meaning in list initialization).

If you want to have arrays of arrays of arrays of various depths and such, then you're going to need to figure out which type that construct needs to be and type it out. auto can only deduce a single level of braced-init-list; you'll need to specify the type(s) explicitly if you need deeper levels.

there is a special exception made for type deduction using auto

Yes, there is. But it only applies to deducing a list for auto itself, not for anything that auto's deduction would require.

In order for auto list = {{1, 2, 3}}; to work, the compiler has to deduce two types: the type to be used for {1, 2, 3} and the type to be used for list. The deduction of the type for list requires deducing the type for the nested braced-init-list. But you can't deduce the type of a braced-init-list. Hence it does not work.

It should also be noted that, even if it did work, it wouldn't actually work. The reason being that the initializer_list inside of list would refer to a temporary array. A temporary array that would be destroyed at the end of the initializing expression. It's basically the same reason why string_view sv = std::string("foo"); doesn't produce something useful.

Why does auto x{3} deduce an initializer_list?

To make long story short:

  • a braced initializer expression {} has no type by itself
  • auto has to infer type information
  • int{3} obviously means "create an int var with value taken from initializer list", thus its type is just int and can be used in any wider context (int i = int{3} will work and auto i = int{3} can deduce type, because right side is obviously of type int)
  • {3} by itself has no type (it can't be int, because it's not a value but an initializer list), so auto wouldn't work — but, because committee considered that auto should still work in this case, they decided that the "best" type for (yeah, typeless by definition) initializer list would be... std::initializer_list, as you already probably guessed.

But, as you pointed out, this made the whole behaviour of auto quite semantically inconsistent. That's why there were proposals to change it — namely N3681, N3912 and N3922 — submitted to the committee. Former proposal was REJECTED as FI3 due to no committee consensus on this matter, http://isocpp.org/files/papers/n3852.html#FI3 , current (N3922) got adopted ca. Q1 of 2015;

tl;dr you may assume that standards-compliant compilers1 with bleeding-edge C++ support2 either have the new, more sane-ish semantics already in place, or will have it shortly.

The Standardization Committee acknowledged the problem by adopting N3922 into draft C++17.

— so it's

auto x1 = { 1, 2 }; // decltype(x1) is std::initializer_list<int>
auto x2 = { 1, 2.0 }; // error: cannot deduce element type
auto x3{ 1, 2 }; // error: not a single element
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 }; // decltype(x5) is int

now, for better or worse.

further reading:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3681.html

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3912.html

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html

http://scottmeyers.blogspot.com/2014/03/if-braced-initializers-have-no-type-why.html

http://herbsutter.com/2014/11/24/updates-to-my-trip-report/


1GCC 5.1 (& up) apparently uses N3922 even in C++11/C++14 mode

2Clang 3.8, with the caveat

This is a backwards-incompatible change that is applied to all language versions that allow type deduction from auto (per the request of the C++ committee).



Related Topics



Leave a reply



Submit