Empty braces magic in initializer lists
Asked Answered
S

2

14

Consider the following minimal example:

#include <iostream>

struct X {
  X() { std::cout << "Default-ctor" << std::endl; }
  X(std::initializer_list<int> l) { 
      std::cout << "Ilist-ctor: " << l.size() << std::endl; 
  }
};

int main() {
    X a{};
    X b({}); // reads as construct from {}
    X c{{}}; // reads as construct from {0}
    X d{{{}}}; // reads as construct from what?
    // X e{{{{}}}}; // fails as expected
}

Godbolt example

I have no questions about a, b and c, everything is rather clear

But I can not understand why d works

What this additional pair of braces in d stands for? I looked up C++20 standard, but I can not find answer easily. Both clang and gcc agree on this code, so it is me who misses something

Socle answered 15/12, 2020 at 1:31 Comment(2)
I'm not certain, but it could be a copy/move from another temporary object initialized via the inner braces (and of course wouldn't actually be a copy/move due to C++17's rules).Layby
@Layby I ran it with C++14 and -fno-elide-constructors and no constructors were called.Vicar
V
19

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
Vicar answered 15/12, 2020 at 1:51 Comment(0)
A
12

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.

Antherozoid answered 15/12, 2020 at 4:16 Comment(1)
Thanks, I already accepted first answer but this is great explanation as well. Btw, I found even more intersting example of brace madness: std::vector<std::string> vs = {{{{{}}}}}; Here, following your explanation string is constructed from {{}} and vector from {{s}} with uniform initialization {v}, wow...Socle

© 2022 - 2024 — McMap. All rights reserved.