Search code examples
c++c++14language-lawyerinitializer-list

Does the standard define list initialization with superfluous braces (e.g. T{{{10}}})?


When using a braced-init-list containing multiple braced-init-list, what are the rules defined by the standard for B, C, and D?

For B, I believe this scenario is defined within the standard as a braced-init-list with a single element and therefore it calls Test(int) directly with no temporary - but I am unable to find where.

For C and D, I am unsure of whether this is undefined behavior or not.

I am also interested in what occurs when using more than a single element i.e. {{{1, 2}}} and if this changes the behavior for B, C, or D?

#include <iostream>

struct Test {
    Test(const int a) {
        // A and B call this
    }

    Test(Test&& test) = delete;
    Test(const Test& test) = delete;
};

int main()
{
    Test a{1}; // calls Test(int)
    Test b{{2}}; // B
    Test c{{{3}}}; // C
    Test d{{{{4}}}}; // D
    // Test e{a}; error, deleted copy constructor
    // Test f{Test{0}}; error, deleted move constructor
    return 0;
}

GCC g++ my_prog.cpp gives me an error for C and D only:

my_prog.cpp: In function 'int main()':
my_prog.cpp:16:17: error: too many braces around initializer for 'int' [-fpermissive]
     Test c{{{3}}};
                 ^
my_prog.cpp:4:14: note:   initializing argument 1 of 'Test::Test(int)'
     Test(int a) {
          ~~~~^

Solution

  • When you have

    Test b{{2}}; 
    

    [dcl.init.list]/3.7 states.

    Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). [...]

    and looking in [over.match] we have [over.match.ctor]/1

    When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization (including default initialization in the context of copy-initialization), the candidate functions are all the converting constructors ([class.conv.ctor]) of that class. The argument list is the expression-list or assignment-expression of the initializer.

    So we consider the constructors, find

    Test(const int a)
    

    and then we use the element {2} as initializer for a which uses [dcl.init.list]/3.9

    Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization); if a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed.

    With

    Test c{{{3}}};
    // and
    Test d{{{{4}}}};
    

    we do the same thing. We look at the constructors and find

    Test(const int a)
    

    as the only viable one. When we do and try to initialize a, we look to [dcl.init.list]/3.9 again but it doesn't apply here. {{3}} and {{{4}}} aren't initializer lists with a single type E. A braced-init-list doesn't have a type so we have to keep going list in [dcl.init.list]/3. When we do we don't meet anything else that matches until [dcl.init.list]/3.12

    Otherwise, the program is ill-formed.