C++模板参数推导中,如何处理非推导上下文的问题?

摘要:最近遇到一个模板参数推导的问题,代码如下: 代码 template<typename T> using scalar = std::enable_if_t<std::is_a
最近遇到一个模板参数推导的问题,代码如下: 代码 template<typename T> using scalar = std::enable_if_t<std::is_arithmetic_v<T>, T>; template<typename T> void foo(scalar<T> val) { ... } foo(5); 这是我突发奇想写出来的,模板别名 scalar 限制函数参数为数值类型,可以在多处复用,这个代码无法通过编译,编译器提示没有匹配的函数调用。 代码很简单,看起来也没什么不妥,为什么出错了?这个看起来简单的问题同样难倒了几个常用的 AI 编程助手(AI 的回答放在文章最后一节)。 问题分析 在正常的模板实例化过程中,编译器结合模板形参模式和实例化时提供的参数类型,确定一个或一组模板实参类型,将这些实参替换到形参后能够形成与实例化参数相匹配的参数列表。 在 foo(5) 这一调用中,形参是 scalar<T>, 实例化参数是 int 类型,编译器需要确定一个类型 T,使得scalar<T> 匹配 int。 我们来理一下这个过程: 我们会说,这不是一眼就能看出 T 就是 int 嘛,std::enable_if<std::is_arithmetic<T>::value, T>::type成功实例化的结果就是 T 本身。但是站在编译器的角度来看,可不能这样下定论,有些情况下,这部分可能并不是一个可以反推出固定类型的模板,举一个最简单的例子: 代码 template<typename T> struct wrapper { using type = int; }; template<typename T> using scalar_confused = typename wrapper<T>::type; template<typename T> void foo_confused(scalar_confused<T> val) { ... } foo_confused(5); 这个模板 wrapper 无论用什么类型实例化都能取到 int,也就是说在反推 T 时无法确定一个唯一的类型,这对于编译器来说是无法处理的,于是它实例化不出任何 foo_confused 的实例。 其实 C++ 标准已经对这类问题作出了说明,官方的命名是非推导上下文(non-deduced context): 我们代码的问题就是上图指出的这种情况,如果模板参数只出现在嵌套名称说明符内(即 :: 符号左边的部分),编译器将不会尝试从实例化参数中推导该模板参数,只能使用已经推导出的或显式指定的参数类型。 StackOverflow 上这篇文章(What is a non deduced context?)还有它提到的一些链接把这个概念讲的很清楚。 如果把 scalar 直接展开到使用位置,我们的代码等价于: 代码 template<typename T> void foo(typename std::enable_if<std::is_arithmetic<T>::value, T>::type val) { ... } 这样看来问题就清晰了,我们拐了一个弯创造了一个非推导上下文。编译器在解析到这一步时,就已经拒绝后续的推导了,后面我们关于反推的分析实际都没有发生。
阅读全文