C++ 聚合 CTAD 中给标量初始化器加花括号是否合法?
按标准规则分析,在支持 C++20 聚合 CTAD 的前提下,题中的两种写法都应推导出 A<double>。GCC、Clang 和 EDG 接受花括号版本,与这一分析一致;MSVC 拒绝该版本、GCC 拒绝圆括号版本,均指向实现问题。这个判断依据标准条文,并非对题中未注明版本的编译器实测得出。
为什么 {1} 可以避开类型推导?
template<typename T>
struct A { T t[2]; };
constexpr A a{ { {1}, 2.5 } };
最外层初始化列表只有一个元素 {{1}, 2.5},对应整个数组成员 t,而不是单独的 t[0]。
当数组成员由 braced-init-list 初始化时,聚合推导候选的对应形参必须是该数组类型的右值引用。可以用下面这个虚构函数模板来理解推导过程:
template<class T>
A<T> deduce(T (&&)[2]);
// 仅用于说明 CTAD 的推导过程:
// deduce({{1}, 2.5});
这个函数只用于解释规则,A 并没有因此获得真实构造函数。对应依据是标准草案的 聚合推导候选规则。
对数组引用形参进行推导时,初始化列表中的元素会被逐个考察:
{1}对应待推导的标量类型T。它是 braced-init-list,不能直接为这里的T提供类型,因此这一项成为非推导语境。2.5是类型为double的表达式,因此推导得到T = double。
第一项无法提供推导结果,但第二项仍能确定 T。这符合 函数调用的模板实参推导规则。
类型确定后,再检查 A<double> 的实际初始化。此时 {1} 用于初始化一个 double,结果是 1.0;第二个元素为 2.5。
为什么删掉花括号反而失败?
删掉 1 外面的花括号后:
constexpr A a{ { 1, 2.5 } };
数组中的两个元素都会参与 T 的推导:1 给出 int,2.5 给出 double。同一个模板参数的推导结果发生冲突,因此推导失败。
模板实参推导和初始化是两个阶段。初始化时可以进行类型转换,但这里的 CTAD 不会自动计算共同类型,也不会先把 1 转成 double 再推导。
标量外面的花括号是否违反标准?
这个例子中的 {1} 合法。目标类型确定为 double 后,它是用单元素初始化列表初始化标量。整数常量 1 能被 double 精确表示,不涉及被禁止的窄化转换。依据可见 列表初始化规则。
Clang 的 braces around scalar initializer 可以理解为对通常显得多余的花括号发出的提醒。不过,这里的花括号确实影响了前面的推导过程,不能直接删除。EDG 的 extra braces are nonstandard 也不能作为这段 C++ 代码不合法的依据。
为什么圆括号版本也应当成立?
constexpr A b( { {1}, 2.5 } );
这里仅将最外层改成了圆括号,内部的 {{1}, 2.5} 仍然是 braced-init-list,也仍然对应数组成员 t。
聚合 CTAD 规则同时覆盖非空花括号初始化列表和圆括号 expression-list,因此对应候选仍应使用 T (&&)[2],推导结果仍为 double。随后,数组成员由内部列表初始化,两个元素分别得到 1.0 和 2.5。
GCC 诊断中出现的 A(T [2]) 及其后的 double*,说明该实现路径没有保留规则要求的数组引用形参。这可以解释报错来源,但不表明圆括号写法本身有问题。
怎样避开这些实现差异?
如果只需要明确得到 A<double>,直接指定模板参数最省事,也不需要保留标量周围的花括号:
template<typename T>
struct A { T t[2]; };
constexpr A<double> a{{1, 2.5}};
static_assert(a.t[0] == 1.0 && a.t[1] == 2.5);
如果希望继续使用自动聚合 CTAD,可以让两个初始化表达式都具有 double 类型:
constexpr A a{{1.0, 2.5}};
static_assert(a.t[0] == 1.0 && a.t[1] == 2.5);
后一种写法需要支持 C++20 聚合 CTAD,但不再依赖 {1} 抑制第一项推导。排查具体编译器时,应记录版本、语言标准模式和完整诊断;题目中的结果不能直接代表这些编译器所有版本的行为。
备注:内容仅供参考。
