Home > Software engineering >  Why can't I use an arbitrary nesting of braces to construct most classes?
Why can't I use an arbitrary nesting of braces to construct most classes?

Time:01-17

Given the following code:

struct A;

struct B {
    B() {}
    B(A &&) {}
};

struct A {
    A() {}
    A(B &&) {}
};

Then I can use as many braces as I want to construct A or B.

// default construct A
auto a = A{};
// default construct B, forward to A
auto b = A{{}};
// default construct A, forward to B, forward to A
auto c = A{{{}}};
// etc.
auto d = A{{{{}}}};
auto e = A{{{{{}}}}};

Similarly, given

struct C {
    C(std::initializer_list<C>) {}
};

then I can also use as many braces as I want

// default construct C
auto f = C{};
// construct from initializer_list of one default constructed C
auto g = C{{}};
// construct from initializer_list of one C constructed from empty initializer_list
auto h = C{{{}}};
// etc.
auto i = C{{{{}}}};
auto j = C{{{{{}}}}};

Why doesn't the same argument work for a truly boring type?

struct D {
};

or, rewritten for clarity:

struct D {
    D() {}
    D(D &&) {}
};

This fails even on

auto k = D{{}};

Why does this not default construct a D with the innermost braces, and then pass that rvalue on to the move constructor of D?

See it live: https://godbolt.org/z/E763EPGh1

CodePudding user response:

There's a special case that precludes D{{}}. It's a very particular set of conditions, so I imagine it's there specifically to prevent this exact recursion.

[over.best.ics]/4 However, if the target is
(4.1) — the first parameter of a constructor
...
and the constructor ... is a candidate by
...
(4.5) — the second phase of [over.match.list] when the initializer list has exactly one element that is itself an initializer list, and the target is the first parameter of a constructor of class X, and the conversion is to X or reference to cv X,
user-defined conversion sequences are not considered.

D{{}} is a list-initialization. D(D&&) constructor is considered by the second phase of it (the first phase looks at initializer-list constructors, like C(std::initializer_list<C>) in your second example). But for it to be viable, there needs to be an implicit conversion from {} to D&&, and [over.best.ics]/4 suppresses it.

CodePudding user response:

Your two versions of D is in fact unequal. Suppose you have

struct E {};
struct F {
    F() {}
    F(F&&) {}
};

auto e = E{{}};
auto f = F{{}};

e fails because E is an aggregate. Each comma separated element within the outermost {} is used to initialize a member in E. Since E contains nothing, you have too many initializers for E.

As @IgorTandetnik mentioned, f fails specifically because it is an exception in implicit conversions: a nested initializer list cannot be used to initialize the reference in copy or move constructors. This is true even if the inner initializer list contains more elements

struct G {
    G() {}
    G(G&&) {}
    G(int, int) {}
};

struct H {
    H() {}
    H(G&&) {}
};

auto g = G{{42, 420}}; // also fails
auto h = H{{42, 420}}; // ok

Presumably, this is to prevent infinite recursions

These rules prevent more than one user-defined conversion from being applied during overload resolution, thereby avoiding infinite recursion.

  •  Tags:  
  • Related