c++language-lawyerc++20aggregate-initializationctad

Why does CTAD fail for an aggregate with a dependent non-array element?


I would like to know if the standard prohibits class template argument deduction (CTAD) for an aggregate with a dependent non-array element in a declaration like this:

auto d1 = D{{1, 2}};

Both GCC 15.1 and Clang 20.1.0 (with -std=c++20 or -std=c++23) fail to compile. The only thing I’ve found on cppreference.com is that

brace elision is not considered for any aggregate element that

has a dependent non-array type

, which would explain d2. However, the example from this blog compiles successfully—even though they don’t explicitly specify the type of the last initializer {5, 6}—so why does

auto d1 = D{{1, 2}};

fail to compile, but

A1 a1{{1,2,3}, 4, {5, 6}};

succeed?

The aggregate deduction candidates are generated according to these rules from cppreference.com:

Let eᵢ be the (possibly recursive) aggregate element that would be initialized from argᵢ, where
...
Otherwise, determine the parameter list T₁, T₂, …, Tₙ of the aggregate deduction candidate as follows:

If eᵢ is an array and argᵢ is a braced-init-list, Tᵢ is an rvalue reference to the declared type of eᵢ.
...
Otherwise, Tᵢ is the declared type of eᵢ.
...

godbolt link

#include <type_traits>

template <typename T>
struct B
{
    T t1;
    T t2;

    // aggregate deduction candidate:
    // template <typename T>
    // B(T, T) -> B<T>;
};

template <typename T>
struct D
{
    B<T> b;

    // aggregate deduction candidate:
    // template <typename T>
    // D(B<T>) -> D<T>;
};

// example from https://oleksandrkvl.github.io/2021/04/02/cpp-20-overview.html#ctad-aggr
// example begin
template <typename T, typename U>
struct Pair
{
    T first;
    U second;

    // aggregate deduction candidate:
    // template <typename T, typename U>
    // Pair(T, U>) -> Pair<T, U>;
};

template <typename T, std::size_t N>
struct A1
{
    T data[N];
    T oneMore;
    Pair<T, T> p;

    // aggregate deduction candidate:
    // template <typename T, std::size_t N>
    // A1(T(&&)[N], T, Pair<T, T>) -> A1<T, N>;
};
// example end

int main()
{
    // auto d1 = D{{1, 2}};     // error: is this declaration legal?
    // auto d2 = D{1, 2};       // error: OK, I expected this to be illegal because of brace elision
    auto d3 = D{B{1, 2}};       // ok
    static_assert(std::is_same_v<decltype(d3), D<int>>);

    // example from https://oleksandrkvl.github.io/2021/04/02/cpp-20-overview.html#ctad-aggr
    // example begin
    // A1::data is an array of dependent bound and A1::p is a dependent type, thus,
    // no brace elision for them
    A1 a1{{1,2,3}, 4, {5, 6}};  // A1<int, 3>
    // example end

    static_assert(std::is_same_v<decltype(a1), A1<int, 3>>);
}

Solution

  • GCC and Clang are right. The aggregate deduction candidate is, as you say, equivalent to

    template <typename T>
    D(B<T>) -> D<T>;
    

    and GCC helpfully prints out this candidate if it is not explicitly declared (with the rest of the diagnostic output being the same in either case). As the quoted wording (paraphrased from [over.match.class.deduct]/1) says, the aggregate deduction candidate has a parameter type T_i for each aggregate element e_i, and T_i is the declared type of e_i if e_i is not of array type.

    As specified by [over.match.class.deduct]/5, the next step is to perform overload resolution as if there were a hypothetical class type D that you were initializing by D{{1, 2}} and the deduction guides were the constructors of D. This hypothetical initialization will use the rules from [over.match.list], that is, to perform a first overload resolution with only initializer-list constructors (of which there are none), and then, when that fails to find any viable functions, take all constructors as candidates in the second stage, where the arguments are the elements of the initializer list. To check whether the aggregate deduction candidate actually produces a candidate for overload resolution, we must perform template argument deduction ([over.match.funcs.general]/8) as specified in [temp.deduct.call]. According to paragraph 1 of that section,

    [...] If removing references and cv-qualifiers from P gives std::initializer_list<P′> or P′[N] for some P′ and N and the argument is a non-empty initializer list ([dcl.init.list]), [...] Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context ([temp.deduct.type]).

    That is, the initializer list argument {1, 2} causes the corresponding parameter B<T> to be a non-deduced context. As a result, T cannot be deduced, there are no viable deduction guides, and class template argument deduction fails.