Empty braces magic in initializer lists

A nice trick to do to get information about what the compiler does, is to compile using all errors: -Weverything. Let's see the output here (for d only):

9.cpp:16:6: warning: constructor call from initializer list is incompatible with C++98                                                                                            
      [-Wc++98-compat]                                                                                                                                                            
  X d{{{}}}; // reads as construct from what?                                                                                                                                     
     ^~~~~~                           

X::X(std::initializer_list) is called.

9.cpp:16:8: warning: scalar initialized from empty initializer list is incompatible with                                                                                          
      C++98 [-Wc++98-compat]                                                                                                                                                      
  X d{{{}}}; // reads as construct from what?                                                                                                                                     
       ^~                               

Scalar (int) initialized in inner {}. So we have X d{{0}}.

9.cpp:16:7: warning: initialization of initializer_list object is incompatible with                                                                                               
      C++98 [-Wc++98-compat]                                                                                                                                                      
  X d{{{}}}; // reads as construct from what?                                                                                                                                     
      ^~~~                                                                                                                                                                        
5 warnings generated.                                                                                                                                                             

std::initializer_list is initialized from {0}. So we have X d{std::initializer_list<int>{0}};!

This shows us everything we need. The extra bracket is for constructing the initializer list.

Note: If you want to add extra brackets you can by invoking the copy/move constructor (or eliding it), but C++ compilers won't do it implicitly for you to prevent errors:

X d{X{{{}}}}; // OK
X e{{{{}}}}; // ERROR

Thought I'd just illustrate:

X d{               {                       {}        }};
   |               |                       |
   construct an    |                       |
   `X` from ...    an initializer_list     |
                   containing...           int{}

The rules for list-initialization are to find an initializer_list<T> constructor and use it if at all possible, otherwise... enumerate the constructors and do the normal thing.

With X{{}}, that is list-initialization: the outermost {}s are the initializer_list and this contains one element: the {}, which is 0. Straightforward enough (though cryptic).

But with X{{{}}}, this doesn't work anymore using the outermost {} as the initializer_list because you can't initialize an int from {{}}. So we fallback to using constructors. Now, one of the constructors takes an initializer_list, so it's kind of like starting over, except that we'd already peeled off one layer of braces.


This is why for instance vector<int>{{1, 2, 3}} works too, not just vector<int>{1, 2, 3}. But like... don't.