Type Deduction and Braced Initializers

I just finished watching a talk from CppCon 2014 by Scott Meyers: Type Deduction and Why You Care. All in all it was a very interesting and entertaining talk, and I learned a thing or two, especially about the combination of type deduction and braced initializers. Since this blog is about simplifying the use of C++, I want to have a short look at that special combination and derive a rule of thumb from it.

A Short Summary of the Topic

The part of the talk that I am referring to in this post starts at about 29:20. After having talked about template type deduction and type deduction for `auto` variables for the more usual cases, Scott explains how type deduction works together with braced initializers. (If you just have watched the video, you can just jump to the next section).

The key point is that braced initializers like `{ 1, 42, 5 }` and `{ 3.14 }` don’t have a type. Therefore template type deduction does not work when they are passed to a function template:

template <class T>
void foo(T t);

int main() {
  foo( { 1, 2, 3, 5, 8 } ); //ERROR
  foo( { 0xBAD } );         //ERROR
}

However, there is a special rule in C++11/14, that `auto` variables which are initialized with such a braced initializer are deduced to be of type `std::initializer_list<X>`, where X is the type of the elements of the initializer. This rule applies regardless of the number of elements and of whether copy initialization (i.e. with `=`) or direct initialization (without `=`) is used:

//C++14
auto a = { 1, 2, 3 }; //initializer_list<int>
auto b { 42 };        //ditto

Then there is a proposal for C++17, N3922, which wants to change the rules: They remain the same for copy initialization, but direct initialization is only allowed with braced initializers that contain only a single element, and the variable then shall have the type of that element:

//N3922
auto a = { 1, 2, 3 }; //std::initializer_list<int>, as before
auto b { 42 };        //NEW: int
auto c { 42, 7 };     //NEW: compile error

The proposal has been adopted into the working draft for C++17, and at least one compiler (Microsoft Visual Studio) already implements that rule. Scott has also written a blog post on this issue.

What to Make of This

As of today, there is only one single sane way I can think of to deal with the mess in this little corner of the language:

Don’t use braced initializers together with type deduction.

Am I simplifying too much? I don’t think so, and here is why:

It Does Not Always Work Anyways

As written above, braced initializers don’t work at all with template type deduction. The same applies for C++11 lambda captures and `decltype`. What remains is `auto` type deduction and C++14’s init-capture for lambdas, which uses the same set of rules. So the next points are about `auto` type deduction:

It’s Not Clear

The syntactic difference between copy initialization and direct initialization is too small for such a huge semantic difference. Anybody reading a line where `auto` is used together with a braced initializer will have to know the rules. For direct initialization she will have to know both rules and which one of them apply to the compiler that is used to compile the code or deduce from the rest of the code which rule might be meant. It’s a maintainability nightmare.

It’s not Portable

Different compilers implement this differently. And not only switching to another compiler might break code that uses direct initialization with braced initializers, upgrading between versions of the same compiler might suffice if the proposal gets accepted into C++17, or in the case of MSVC, if it does not get accepted and the current behavior is removed from the compiler.

It’s a Corner Case

There is little or no use for `std::initializer_list` as standalone variables, at least as far as I know of (I would love to hear from sensible real world examples in the comments). If there are occasions where a variable of the type is needed, the programmer should explicitly use the type instead of `auto`.

“Wait, what?? No `auto`?” Not this time. `auto` is great for standard cases where the exact type of a variable is not necessary to know or where it is easy to derive from the context. In this case, where you want to use a not so usual type, document it. If you insist on using auto, initialize your `auto` variable with an explicitly constructed `std::initializer_list`:

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

That way every reader of the code will know that you indeed meant to use an `initializer_list` and did not just happen to fall into a trap that type deduction rules made for you.

Update 21.02.2015:

Scott Meyers has written a new blog post, sheding some light on the rationale behind the auto deduction rules for braced initialitzers.

Previous Post
Next Post
Posted in

7 Comments


  1. The curious question is whether this is considered a C++14 defect or a C++1z feature? I recently answered an SO question about this: http://stackoverflow.com/a/31301568/1708801 and gcc implements N3922 in C++14 mode but clang does not implement N3922 yet and their status page lists it as a C++1z proposal which is consistent with your blog but N3922 recommends that this should be considered a C++14 defect.

    Reply

    1. Thanks for your thoughts, Shafik.
      Proposals are not only used to implement new features but also to fix defects. On the contrary, defect reports usually at some time add a proposal how to fix the defect, so N3922 contains both the DR and the porposal for a fix, which is not contradicting. However, I still consider M3922 not to be satisfactory, because it is simply unintuitive and adds an unneeded WTF to the language for reasons that seem to be corner cases. See also my comment on Scott Meyers’ post I mention in the last section.

      Reply



  2. gcc also implements N3922, and will ship it soon in gcc5.

    Reply

  3. The only “useful” case I can think of is a literal range, like:
    for (auto i : { 1, 2, 4, 8, 10 })
    {
    }

    But { 1, 2, 4, 8, 10 } could also be a variable previously initialized, and you wouldn’t care if it was an initializer_list or anything else iterable (like a vector, a string, etc.).

    Reply

    1. Hi, thanks for your thoughts.

      I commented on Scott’s new blog post, proposing to disallow copy initialization of auto variables with braced initializers, but leaving N3922 for direct initialization. That would mean to only allow uniform initialization style for auto variables and never deducing initializer_list. To compensate, a new library function á la make_tuple could be used:

      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, 3); //initializer_list

      Such a function could be used for range based for loops as well:

      for ( i : make_initializer_list(1, 2, 4, 8, 10) )

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *