c++language-lawyerc++20designated-initializer

Why does this code using designated initializers in function parameters goes from ambiguous to not compiling when removing one function?


Consider the following code:

struct A{
    int x;
    int y;
};
struct B{
    int y;
    int x;
};

void func (A){
}
void func (B){
}

int main()
{
    func({.y=1,.x=1});
}

For some reason both clang and gcc consider this code ambiguous even though it is known that order in designated initializer must match the order in struct, although for some reason clang allows it and just issues a warning:

ISO C++ requires field designators to be specified in declaration order; field 'y' will be initialized after field 'x' [-Wreorder-init-list]

Now the fun begins: If I comment out the func(B) code goes from ambiguous to not compiling.

That is what I consider super weird. Is there some logic behind this?

If the cause of my confusion is not clear:

if we have both func(A) and func(B) in code both gcc and clang consider the func({.y=1,.x=1}) ambiguous, but if I remove func(B) from source then it gives an error(or warning in case of clang). In other words we went from 2 options to 0 options by removing 1 option.

godbolt


Solution

  • The rule is in [over.ics.list]/2:

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

    [Note 1: Aggregate initialization does not require that the members are declared in designation order. If, after overload resolution, the order does not match for the selected overload, the initialization of the parameter will be ill-formed ([dcl.init.list]).

    [Example 1:

    struct A { int x, y; };
    struct B { int y, x; };
    
    void f(A a, int);               // #1
    void f(B b, ...);               // #2
    
    void g(A a);                    // #3
    void g(B b);                    // #4
    
    void h() {
      f({.x = 1, .y = 2}, 0);       // OK; calls #1
      f({.y = 2, .x = 1}, 0);       // error: selects #1, initialization of a fails
                                    // due to non-matching member order ([dcl.init.list])
      g({.x = 1, .y = 2});          // error: ambiguous between #3 and #4
    }
    

    end example]

    end note]

    Basically, the rule that the designators have to name members is in [dcl.init.aggr] (so it counts for whether you can form a conversion sequence) but the rule that designators have to be in order is in [dcl.init.list] (so it's just like a later requirement that happens, and does not affect whether a conversion sequence can be formed - so it's not "sfinae-friendly").

    In other words, this is fine:

    struct A { int a; };
    struct B { int b; };
    ​
    void f(A);
    void f(B);
    ​
    void call_f() {
        f({.a=1}); // ok, calls f(A)
    }
    

    Because you cannot initialize a B from {.a=1} according to the rules for aggregate initialization.

    But your example (and the one quoted above) are ambiguous, since both satisfy the aggregate initialization rules but fail later.