实例化C++可变参数函数模板时的奇怪编译错误问题
问题成因
这个现象是两方面共同作用的结果:一是示例代码触发了C++标准中极少被用到的「非末尾位置函数参数包」边缘规则,二是三个编译器对这个边缘场景的实现都存在不同程度的缺陷或非标准扩展,最终出现了完全矛盾的表现。
对应的C++标准规则
- 函数参数列表基础语法要求:如果一个函数参数包出现在参数列表中,它之后的所有参数要么是另一个参数包,要么必须带有默认实参。示例中
std::type_identity_t<TT>...是明确的函数参数包,它后面的std::type_identity_t<T>是单个非包形参,且没有默认实参,严格来说这个函数声明本身就不符合语法要求,属于非良构代码。
编译器没有在声明阶段直接报错的原因是,这个参数包的长度依赖于模板参数,属于模板解析阶段的边缘场景,三大编译器都没有做对应的声明期诊断。 - 模板推导规则:用
std::type_identity_t将后续形参包裹为非推导上下文的写法本身是正确的,按照设计,TT应该完全从第一个Pack<TT...>形参推导,T完全从第二个Pack<T>形参推导,推导完成后就能确定后续形参的总数量是sizeof...(TT) + 1,此时只需要校验传入实参的数量、类型是否匹配即可。
各编译器表现的具体原因
- Clang:Clang错误地将非推导上下文里的
TT...形参包纳入了模板推导范围,默认给这个非末尾位置的参数包推导了空长度,和从第一个形参推导出的TT = {int}长度冲突,因此直接拒绝所有调用,给出的「TT推导长度不一致」报错是推导逻辑错误导致的。 - GCC:GCC在处理调用时出现了内部逻辑不一致:计算实参数量是否匹配时,先默认非末尾的形参包长度为0,算出总形参数是3,因此传4个实参时提示参数过多;但输出错误信息时又正确读取了从第一个形参推导出的
TT = {int}结果,算出总形参数是4,因此传3个实参时又提示参数不足,属于明确的实现bug。 - MSVC:MSVC对这个场景做了非标准的宽松扩展,没有死板地将非末尾的非推导形参包判定为空长度,而是正确使用了从前面形参推导出的
TT长度来计算形参总数,因此4个实参的正确调用可以通过编译;但它的校验逻辑过于宽松,连3个实参的错误调用也没有拦截,属于扩展过度。
符合标准的正确写法
如果要写出所有编译器都能一致处理的合法代码,不要将参数包放在非末尾的非默认参数位置,可以选择两种改法:
- 将后续参数统一打包传递,避免非末尾参数包:
template<typename... TT, typename T> void f(Pack<TT...>, Pack<T>, Pack<std::type_identity_t<TT>..., std::type_identity_t<T>>); // 调用方式 f(Pack<int>{}, Pack<int>{}, Pack<int, int>{5,5});
- 用C++20约束校验实参,将参数包放到形参列表末尾:
template<typename... TT, typename T, typename... Args> void f(Pack<TT...>, Pack<T>, Args... args) requires (sizeof...(Args) == sizeof...(TT) + 1) && (std::is_same_v<Args, TT> && ...) && std::is_same_v<std::tuple_element_t<sizeof...(TT), std::tuple<Args...>>, T> { // 函数实现 }
内容的提问来源于stack exchange,提问作者md1357
相关产品推荐
相关产品推荐

