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
For s1:
e₁ is t
(type T[3]
).
arg₁ is the string literal "hi"
.
By the second quote, T₁ should be const T(&)[3]
.
so I expected CTAD to deduce T = char
.
For s2:
Since e₁’s element type is dependent and arg₁ is a string literal, brace-elision should be disallowed, so I expected auto s2 = S{"hi"};
to be ill-formed.
What actually happens
s1
deduces S<const char*>
.
s2
compiles without error, also deducing S<const char*>
.
Questions for the community
Is this behavior correct and intentional according to the C++ standard?
Is there any way to force CTAD to deduce S<char>
without providing a deduction guide or specifying the template argument (e.g. S<char>{"hi"}
)?
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.
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.