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

Why does CTAD deduce S<const char*> (not S<char>) and allow auto s2 = S{"hi"};?


I’m investigating class template argument deduction (CTAD) for an aggregate with an array element when initialized from a string literal.

#include <type_traits>
#include <iostream>

template <typename T>
struct S {
    T t[3];
    /*
      aggregate deduction candidate:
      template<typename T>
      S(const T(&)[3]) -> S<T>;
    */
};

int main() {
    auto s1 = S{{"hi"}};   // I expected decltype(s1) == S<char>
    static_assert(std::is_same_v<decltype(s1), S<const char*>>);  // passes

    auto s2 = S{"hi"};     // I expected this to be a compilation error

    auto s3 = S<char>{"hi"};    // OK: prints "hi"
    auto s4 = S<char>{{"hi"}};  // OK: prints "hi"
}

Quotes from cppreference.com

Let ei be the (possibly recursive) aggregate element that would be initialized from argi, where

Otherwise, determine the parameter list T1, T2, …, Tn of the aggregate deduction candidate as follows:

If ei is an array and argi is a string literal, Ti is an lvalue reference to the const-qualified declared type of ei.

Brace elision is not considered for any aggregate element that has

an array type with a dependent array element type and argi is a string literal

What I expected

What actually happens

Questions for the community

Edit 1

The code was tested with GCC 15.1 and Clang 20.1.0 using the compiler flags -std=c++20 and -std=c++23.


Solution

  • According to cppreference, the parameter list T1, T2, ..., Tn of the aggregate deduction candidate is determined as follows:

    • If ei is an array and argi is a braced-init-list, Ti is an rvalue reference to the declared type of ei.
    • If ei is an array and argi is a string literal, Ti is an lvalue reference to the const-qualified declared type of ei.
    • Otherwise, Ti is the declared type of ei.

    In the case of S{{"hi"}}, arg1 is not a string literal: it is the braced-init-list {"hi"}. Thus, the first bullet applies (which says Ti is an rvalue reference), and the aggregate deduction candidate is equivalent to:

    template<typename T>
    S(T (&&t)[3]) -> S<T>;
    

    Then, since the argument {"hi"} is a braced-init-list, template argument deduction is performed for each of its elements, taking T as the parameter and the list element as the argument. Deducing T from "hi" produces T = const char*.

    In the case of S{"hi"}, if brace elision is not considered, then arg1 is "hi". The second bullet applies. The aggregate deduction candidate is equivalent to:

    template<typename T>
    S(const T (&t)[3]) -> S<T>;
    

    Deducing const T[3] from "hi" produces T = char.

    However, neither GCC nor Clang implements CWG 2685, which clarifies that brace elision is not considered for any array with a dependent element type and argi is a string literal. These compilers will consider brace elision and treat S{"hi"} the same as S{{"hi"}}. MSVC, on the other hand, will consider S{"hi"} to have type S<char> as expected.