Clang与GCC重载函数模板歧义判定差异及C++11解决方案
C++ tuple遍历函数的编译器兼容性问题
我尝试将最初为GCC 8.2编写的代码移植为可被Clang正常编译的版本,代码实现如下:
#include <tuple> struct Q{}; using TUP = std::tuple<Q>; template<typename Fn> inline void feh(Fn&, const std::tuple<>*) {} template<typename Fn, typename H> inline void feh(Fn& fn, const std::tuple<H>*) { fn(H{}); } template<typename Fn, typename H, typename... R> inline void feh(Fn& fn, const std::tuple<H, R...>*) { fn(H{}); using Rest = const std::tuple<R...>*; feh<Fn, R...>(fn, static_cast<Rest>(nullptr)); } template<typename Tuple, typename Fn> inline void fe(Fn& fn, const Tuple * tpl = nullptr) { feh(fn, tpl); } int main() { auto r = [] (Q const&) {}; TUP tup; fe<TUP>(r, &tup); }
GCC 8.2及12.1版本均可正常编译上述代码,但Clang 11.0.0、14.0.0版本会抛出编译错误,提示fe函数内对feh的调用存在歧义,两个候选重载分别为:void feh(Fn& fn, const std::tuple<H>*) [with Fn = (lambda at <source>:38:14), H = Q]
与void feh(Fn& fn, const std::tuple<H, R...>*) [with Fn = (lambda at <source>:38:14), H = Q, R = <>]
该问题可通过主流在线C++编译平台复现。
现有两个疑问:
- 两款编译器的判定哪一个符合C++标准要求?
- 如何修改代码可同时被两款编译器正常编译?
说明:C17标准引入的if constexpr、折叠表达式可实现相关逻辑,但该代码为供多项目引用的库头文件,部分引用项目未启用C17编译,因此需要可在C++11标准下正常工作的解决方案。
解答
1. 标准符合性结论
Clang的判定完全符合C标准要求,GCC此处属于非标准的宽松实现行为。
根据C模板重载的偏序规则,当传入的实参是std::tuple<Q>*类型时:
- 单参数的
feh<Fn, H>重载可以匹配,推导H=Q - 变参的
feh<Fn, H, R...>重载同样可以匹配,推导H=Q、参数包R...为空,实例化后的形参类型同样是const std::tuple<Q>*
两个重载的匹配优先级完全相同,不存在哪个模板更特化的区别,标准要求这种歧义场景下编译器必须抛出编译错误。GCC额外做了非标准的优先级判断,优先选择了非变参重载才未报错,该行为不具备可移植性。
2. C++11兼容修改方案
核心修改思路是从模板定义层面消除重载匹配的重叠区间,让变参版本的feh仅在tuple元素个数≥2时才会参与匹配,从根源上避免歧义。
修改后的完整代码如下:
#include <tuple> struct Q{}; using TUP = std::tuple<Q>; template<typename Fn> inline void feh(Fn&, const std::tuple<>*) {} template<typename Fn, typename H> inline void feh(Fn& fn, const std::tuple<H>*) { fn(H{}); } // 新增R1模板参数,强制该重载仅匹配元素数≥2的tuple template<typename Fn, typename H, typename R1, typename... R> inline void feh(Fn& fn, const std::tuple<H, R1, R...>*) { fn(H{}); using Rest = const std::tuple<R1, R...>*; // 递归调用同步调整模板参数传递 feh<Fn, R1, R...>(fn, static_cast<Rest>(nullptr)); } template<typename Tuple, typename Fn> inline void fe(Fn& fn, const Tuple * tpl = nullptr) { feh(fn, tpl); } int main() { auto r = [] (Q const&) {}; TUP tup; fe<TUP>(r, &tup); }
修改点说明
- 将原变参版本的模板参数从
<typename Fn, typename H, typename... R>调整为<typename Fn, typename H, typename R1, typename... R>,强制要求该重载匹配的tuple至少包含2个元素,和空tuple、单元素tuple的重载完全没有匹配重叠 - 递归调用
feh时同步调整传入的模板参数为R1, R...,保证递归遍历的逻辑和原代码完全等价 - 该实现完全遵循C11标准,未使用任何高版本特性,所有支持C11的GCC、Clang版本均可正常编译,运行逻辑和原代码完全一致。
内容的提问来源于stack exchange,提问作者Bulletmagnet
相关产品推荐
相关产品推荐

