c++language-lawyerstdvectorinitializer-listoverload-resolution

Construction from nested brace-enclosed initializer list


I have a working program that compiles successfully in Visual Studio. But trying to port it to GCC/Clang resulted in some compilation errors.

Maximally reduced example is as follows:

#include <vector>

struct A {
    A(const std::vector<int> &) {}
};

A a( { { 1, 2 } } );

Both GCC and Clang complain:

error: call of overloaded 'A(<brace-enclosed initializer list>)' is ambiguous
    7 | A a( { { 1, 2 } } );
      |                   ^
note: there are 3 candidates
note: candidate 1: 'A::A(const std::vector<int>&)'
note: candidate 2: 'constexpr A::A(const A&)'
note: candidate 3: 'constexpr A::A(A&&)'

But EDG agrees with MSVC that the program is ok. Online demo: https://gcc.godbolt.org/z/Msjc9f1nK

Is there real ambiguity here and which implementation is correct?


Trying to simplify the program even more and get rid of std::vector, I came to another example:

struct B {
    int i, j;
};

struct A {
    A(A&&) = delete;
    A(const B &) {}
};

A a( { { 1, 2 } } );

This one is accepted by GCC and EDG. But both Clang and MSVC reject it with

error: call to deleted constructor of 'A'
error C2280: 'A::A(A &&)': attempting to reference a deleted function

Online demo: https://gcc.godbolt.org/z/cThfajaa8

The same question: what behavior is correct?


Solution

  • Your 2 examples are somewhat related but aren't equivalent, and the discrepancies arise due to different reasons.

    First example

    #include <vector>
    
    struct A {
        A(const std::vector<int> &) {}
    };
    
    A a( { { 1, 2 } } );
    

    As GCC/Clang error messages say, there are 3 candidate constructors, and they are chosen by [over.match.ctor] (bold emphasis and ellipsis omissions mine):

    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 (including default-initialization in the context of copy-list-initialization), the candidate functions are all the constructors of the class of the object being initialized. Otherwise, the candidate functions are all the non-explicit constructors ([class.conv.ctor]) of that class. The argument list is the expression-list or assignment-expression of the initializer. ...

    [over.match.viable]/4 says:

    Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence that converts that argument to the corresponding parameter of F. ...

    Since the argument is an initializer list, [over.ics.list]/1 applies:

    When an argument is an initializer list ([dcl.init.list]), it is not an expression and special rules apply for converting it to a parameter type.

    [over.ics.list]/9 then immediately redirects:

    Otherwise, if the parameter is a reference, see [over.ics.ref]

    [Note 2: The rules in this subclause will apply for initializing the underlying temporary for the reference. — end note]

    Now, [over.ics.ref]/1 says:

    When a parameter of type “reference to cv T” binds directly ([dcl.init.ref]) to an argument expression:

    • ...
    • Otherwise, if the type of the argument is possibly cv-qualified T, or ..., the implicit conversion sequence is the identity conversion.
    • ...
    • ...

    If the parameter binds directly to the result of applying a conversion function to the argument expression, the implicit conversion sequence is a user-defined conversion sequence ([over.ics.user]) whose second standard conversion sequence is determined by the above rules.

    and [over.ics.ref]/2 says:

    When a parameter of reference type is not bound directly to an argument expression, the conversion sequence is the one required to convert the argument expression to the referenced type according to [over.best.ics]. Conceptually, this conversion sequence corresponds to copy-initializing a temporary of the referenced type with the argument expression. ...

    Here we have a problem. { { 1, 2 } } isn't an expression, so we can neither "bind directly" to nor convert "the argument expression" to anything, even if "is not bound directly to an argument expression" (in [over.ics.ref]/2) is to be understood to include the case where the argument isn't an expression at all. The wording of [over.ics.ref] in general is imprecise, because it's written as if it's not intended to be used when argument isn't an expression, even though [over.ics.list]/9 clearly redirects to it. Besides, the notion of "bound directly" is defined only in [dcl.init.ref], not in [dcl.init.list] (governing initialization of both object and references from braced-init-lists), which would have to be referenced from [over.ics.ref] if redirection in [over.ics.list]/9 remains intact (or this redirection can be removed and references handled in [over.ics.list] directly). This defect is the point of the still active CWG 1536.

    Note that the example in [over.ics.list]/9 supports that this (quite common) case shouldn't be an error (and all compilers agree here), but it doesn't show in the comments what the intended implicit conversion sequence is in its entirety, only saying that it contains a user-defined conversion. But user-defined conversion sequences contain a second standard conversion sequence (which, in particular, may contain reference binding, see [over.ics.user]/2), which could influence which conversion sequence is considered better.

    Anyway, the intention (see Note 2 from [over.ics.list]/9) is that we, in one way or another, get to initializing parameter's referenced type with an argument braced-init-list, so we get to [over.ics.list]/7:

    Otherwise, if the parameter is a non-aggregate class X and overload resolution per [over.match.list] chooses a single best constructor C of X to perform the initialization of an object of type X from the argument initializer list:

    • If C is not an initializer-list constructor and the initializer list has a single element of type cv U ...
    • Otherwise, the implicit conversion sequence is a user-defined conversion sequence whose second standard conversion sequence is an identity conversion.

    (note that the first bullet doesn't apply because {1, 2}, being not an expression, doesn't have 'a type')

    and [over.match.list]:

    When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause or when forming a list-initialization sequence according to [over.ics.list], overload resolution selects the constructor in two phases:

    • If the initializer list is not empty or T has no default constructor, overload resolution is first performed where the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.
    • Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

    In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

    Now, depending on which of the 3 candidates we currently consider:

    In all cases the second standard conversion sequences, due to the issue described above, are underspecified (though are most likely/sanely to be interpreted to contain identity conversion and appropriate reference binding, interpreted as binding to rvalue).

    Now, all our constructors are viable, and when choosing the best of them, we, according to [over.match.best.general]/2, need to compare ICS for corresponding arguments:

    Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2) , and then

    • for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that,
    • ...

    Then [over.ics.rank]/3 says:

    Two implicit conversion sequences of the same form are indistinguishable conversion sequences unless one of the following rules applies:

    • ...
    • Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if
      • ...
      • S1 and S2 include reference bindings ([dcl.init.ref]) and neither refers to an implicit object parameter of a non-static member function declared without a ref-qualifier, and S1 binds an rvalue reference to an rvalue and S2 binds an lvalue reference
      • ...
    • User-defined conversion sequence U1 is a better conversion sequence than another user-defined conversion sequence U2 if they contain the same user-defined conversion function or constructor or they initialize the same class in an aggregate initialization and in either case the second standard conversion sequence of U1 is better than the second standard conversion sequence of U2.

    The bold parts apply (assuming the second standard conversion sequences are interpreted as described above) when comparing ICS for the arguments of A(A&&) and A(const A&), therefore the move constructor is better than the copy constructor (as expected). However, when comparing these ICS to ICS for the argument of A(const std::vector<int> &), we can't apply the comparison by reference binding, because the underlying constructors (A(const std::vector<int> &) and vector initializer list constructor, respectively) are different. As a result, there's nothing making A(const std::vector<int> &) better or worse than A(A&&), and the overload resolution is ambiguous.

    Note that we can isolate the compiler discrepancy to not include the aforementioned issue of reference binding to initializer lists like this (godbolt):

    struct E {
        E(int) {}
    };
    
    struct F {
        F(int) {}
    };
    
    struct D {
        D(F&&, int) {}
        D(const E&, int) {}
    };
    
    D d (4, 2);
    

    (note we can use dummy int parameter/argument to immediately make copy/move constructors of D non-viable by number of parameters for simpler analysis, though even without that we'd have [over.best.ics.general]/4.3 making them non-viable)

    Here, the reasoning wouldn't stumble on CWG 1536, but we still have the same result - GCC and Clang correctly reject, MSVC and EDG wrongly accept and choose D(F&&, int) (as you can see from assembly listings or by adding console output to constructors and executing the code), most likely due to misapplying [over.ics.rank]/3.3 even when constructors or conversion functions in compared ICS' are different (because, when you change D(const E&, int) to D(E&&, int), all compilers correctly reject and report ambiguity).

    Second example

    struct B {
        int i, j;
    };
    
    struct A {
        A(A&&) = delete;
        A(const B &) {}
    };
    
    A a( { { 1, 2 } } );
    

    The important difference here is that B is an aggregate directly containing two int's and can't be initialized from { { 1, 2 } } as a temporary referenced by the parameter of A(const B&), so ICS for this argument can't be formed and thus constructor isn't viable. The ICS can only be formed with argument { 1, 2 } (save for the aforementioned issue for references arising from CWG 1536), which can happen when ICS forms for the first argument of copy/move constructor of A with argument { { 1, 2 } }. You can easily see this by introducing dummy parameter/argument as in my example above to not consider copy/move constructors and seeing that all compilers correctly reject it.

    For completeness, the relevant excerpts are [over.ics.list]/8:

    Otherwise, if the parameter has an aggregate type which can be initialized from the initializer list according to the rules for aggregate initialization ([dcl.init.aggr]), the implicit conversion sequence is a user-defined conversion sequence whose second standard conversion sequence is an identity conversion.

    , [dcl.init.aggr]/3:

    When an aggregate is initialized by an initializer list as specified in [dcl.init.list], the elements of the initializer list are taken as initializers for the elements of the aggregate. The explicitly initialized elements of the aggregate are determined as follows:

    • ...
    • If the initializer list is a brace-enclosed initializer-list, the explicitly initialized elements of the aggregate are those for which an element of the initializer list appertains to the aggregate element or to a subobject thereof (see below).
    • ...

    , [dcl.init.aggr]/4:

    For each explicitly initialized element:

    • ...
    • Otherwise, the initializer list is a brace-enclosed initializer-list. If an initializer-clause appertains to the aggregate element, then the aggregate element is copy-initialized from the initializer-clause. ...
    • ...

    and [dcl.init.aggr]/14:

    Each initializer-clause in a brace-enclosed initializer-list is said to appertain to an element of the aggregate being initialized or to an element of one of its subaggregates. Considering the sequence of initializer-clauses, and the sequence of aggregate elements initially formed as the sequence of elements of the aggregate being initialized and potentially modified as described below, each initializer-clause appertains to the corresponding aggregate element if

    • the aggregate element is not an aggregate, or
    • the initializer-clause begins with a left brace, or
    • ...

    ...

    (clearly, when B is aggregate-initialized from { { 1, 2 } }, { 1, 2 } appertains to B::i, but an int can't be copy-(list)-initialized from it. When B is aggregate-initialized from { 1, 2 }, B::i appertains to 1, B::j - to 2, and the aggregate-initialization can happpen)

    Note that ICS for the first parameter of A(A&&)/A(const A&) can only form using A(const B&), because when 'recursively' considering A(A&&)/A(const A&) for this purpose, they wouldn't be viable due to [over.best.ics.general]/4 disallowing user-defined conversion for its parameter:

    However, if the target is

    • the first parameter of a constructor or
    • the object parameter of a user-defined conversion function

    and the constructor or user-defined conversion function is a candidate by

    • [over.match.ctor], when the argument is the temporary in the second step of a class copy-initialization,
    • [over.match.copy], [over.match.conv], or [over.match.ref] (in all cases), or
    • 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.

    (though, even without that, using A(A&&)/A(const A&) 'recursively' would require A to have a constructor from two elements, which it doesn't)

    So, we see that only copy/move constructors can be viable for initialization of a, and of them, the A(A&&) is the best because it binds to rvalue and is better due to [over.ics.rank]/3.2.3 (again, though, see CWG 1536 discussion above, because { { 1, 2 } } isn't an expression and technically can't be an rvalue), so overload resolution succeeds choosing A(A&&). Clang and MSVC are thus correct to reject (because the constructor is deleted), and GCC and EDG are wrong.

    As for the reasons why this happens, this probably has to do with how the compilers implement guaranteed copy elision introduced with C++17. If we choose the standard in your example to be C++14 for GCC and EDG, they start correctly rejecting the code (godbolt), while for any C++17+ they wrongly accept. But guaranteed copy elision shouldn't apply here, because the first bullet point in [dcl.init.general]/16.6 doesn't apply:

    Otherwise, if the destination type is a class type:

    • If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same as the destination type, the initializer expression is used to initialize the destination object.
    • ...

    Also note that if a constructor call is omitted due to non-mandatory copy elision (per [class.copy.elision]) there's nothing that would allow the constructor to be deleted (so, if GCC and EDG somehow used this clause for our case, they would still have to reject the code). Besides, the non-mandatory copy elisions aren't allowed here, because the relevant clause for copying/moving a temporary, which existed in C++14 ([class.copy]/31.3 of C++14) doesn't exist since C++17 (see [class.copy.elision] of C++17).

    Since there is an active CWG 2327 "Copy elision for direct-initialization with a conversion function" and P2828R2 addressing it (which adds guaranteed copy elision when copy/move constructor's parameter binds to the result of a conversion function to the same class), I opened an issue to, potentially, introduce guaranteed copy elision in cases like your example (when copy/move constructor's parameter binds to the object constructed by (another) constructor of the same class) as well, following what GCC and EDG do.