c++language-lawyeroverload-resolutionaggregate-initialization

Overload resolution for implicit conversion operators used in aggregate Initialization


Consider the following setup:

struct MyInt {
    template<class T>
    operator T() = delete;

    operator int(){ return 42; }
};

struct TestCArr {
    int arr[2];
};

struct TestStdArr {
    std::array<int, 2> arr;
};

MyInt here is implicitly convertible to int, but all other conversions are deleted.

The following compiles as expected with all major compilers using aggreagate initialization to initialize arr[0] with MyInt{} via operator int():

TestCArr{ MyInt{} };

But what is the expected behavior of the following statement?

TestStdArr{ MyInt{} };

To my understanding, this should not compile: MyInt has a conversion operator to std::array<int, 2>, so that should be the best fit. But that operator is deleted, so I'd expect a compile time error. (The difference to before is that it is impossible to have a conversion operator to int[2], so the conversion to int is used.)

MSVC and gcc seem to agree with me, but clang compiles the statement, picking the conversion operator to int and aggregate initializes the first member of arr (see https://godbolt.org/z/dK4ronrGv).

So is my understanding correct that operator std::array<int, 2>() is a better fit than operator int() and therefore has a higher priority, even though it is deleted?

P.S. If I remove the deleted operator T() from MyInt, the above statement compiles with all three major compilers, all choosing operator int() to perform aggregate initialization: https://godbolt.org/z/sYr7ff793

P.P.S If I use the following definition of MyInt, the behavior of all compilers stays the same (removing arguments concerning templates vs non-templates and more constrained vs less constrained): https://godbolt.org/z/cvhMj9xPq

struct MyInt {
    template<class T>
      requires (!std::integral<T>)
    operator T() = delete;


    template<class T>
      requires std::integral<T>
    operator T() { return 42; };
};

Solution

  • I think clang is wrong here.

    List initialization supports brace elision. There a mechanism by which you figure out if an initializer refers to the current element or a sub-element of that element. That rule is [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

    1. the aggregate element is not an aggregate, or
    2. the initializer-clause begins with a left brace, or
    3. the initializer-clause is an expression and an implicit conversion sequence can be formed that converts the expression to the type of the aggregate element, or
    4. the aggregate element is an aggregate that itself has no aggregate elements. Otherwise, the aggregate element is an aggregate and that subaggregate is replaced in the list of aggregate elements by the sequence of its own aggregate elements, and the appertainment analysis resumes with the first such element and the same initializer-clause.

    So when you do TestCArr{ MyInt{} } for instance, we first try to see if MyInt{} appertains to int arr[2]. And it doesn't. (1) this is an aggregate, (2) our initializer-clause does not begin with a left brace, (3) the initializer-clause is an expression but we cannot form a conversion sequence to int[2], and (4) int[2] isn't empty. So we recurse and consider the first element of the int[2], which gets initialized.

    But when you do TestStdArr{ MyInt{} }, the logic isn't quite the same. In (3), we can form an implicit conversion sequence to std::array<int, 2>. That conversion function is deleted, but that still counts as an implicit conversion sequence (which is important because otherwise deleted wouldn't really work the way as intended...). So we don't recurse into the sub-aggregates of std::array<int, 2>, instead directly initializing the std::array. Which should fail.

    gcc and msvc get this right.