别名模板非包参数的包展开:依赖名称影响与合规性差异原因
错误代码示例
以下代码在GCC、Clang和MSVC中均会触发编译错误:
#include <cstdint> #include <concepts> template <std::size_t N> struct a { template <class T0, class...Ts> using f = T0; }; template <std::size_t N> struct b { template <class... Ts> using f = typename a<N>::template f<Ts...>; }; static_assert(std::same_as<b<0>::f<int,char,bool>, int>); int main() { }
编译错误信息
<source>:13:41: error: pack expansion used as argument for non-pack parameter of alias template 13 | using f = typename a<N>::template f<Ts...>; | ^~~~~ <source>:12:5: note: in instantiation of template type alias 'f' requested here 12 | template <class... Ts> | ^ <source>:16:28: note: in instantiation of template class 'b<0>' requested here 16 | static_assert(std::same_as<b<0>::f<int,char,bool>, int>); | ^ <source>:6:17: note: template parameter is declared here 6 | template <class T0, class...Ts> | ^
修复后的代码
若将a的实例化修改为依赖于Ts...,则代码可正常编译:
template <std::size_t N> struct b { template <class... Ts> using f = typename a<((void)sizeof...(Ts),N)>::template f<Ts...>; };
简化复现代码
可以通过更简洁的代码复现该问题:
template <typename T, typename... P> using type1 = T; template <typename... P> using type2 = type1<P...>;
对应的编译错误:
error: pack expansion argument for non-pack parameter 'T' of alias template 'template<class T, class ... P> using type1 = T'
using type2 = type1<P...>;
注:该问题仅出现在别名模板中,普通类模板无此问题。
核心疑问
管控该差异的C++相关规则是什么?即为何依赖Ts的a特化版本是良构的,而不依赖的版本是非良构的?
规则解释
这一行为由C++模板的实参推断与依赖名称查找规则管控,核心要点如下:
别名模板的实参检查时机
别名模板的定义阶段,编译器会尽可能提前做实参兼容性检查。原始错误代码中,a<N>不依赖于b的模板参数Ts...,编译器能确定a<N>::f的参数列表是「第一个为非包参数T0、后续为包参数Ts」。此时将完整的Ts...包展开传递给f,编译器会判定你试图用包展开匹配非包参数T0——这违反规则,因为非包参数不能接收包展开结果(编译器无法在定义阶段保证包仅有一个元素)。依赖模板参数的名称延迟检查
修改为a<((void)sizeof...(Ts),N)>后,a的实参依赖于Ts...,此时typename a<...>::template f成为依赖模板参数的名称。根据C++规则,这类名称的具体参数列表要等到模板实例化阶段才能确定,编译器不会在定义阶段提前检查实参匹配性。当实例化b<0>::f<int,char,bool>时,Ts...展开为int,char,bool,恰好匹配f的T0=int、Ts...=char,bool的参数形式,因此编译通过。普通类模板与别名模板的差异
普通类模板的实参检查仅在实例化阶段进行,而别名模板作为类型别名的模板化版本,编译器会在定义阶段做更严格的前置检查,这就是普通类模板不会出现此问题的原因。
内容的提问来源于stack exchange,提问作者TartanLlama

