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)
{
...
}
这样看来问题就清晰了,我们拐了一个弯创造了一个非推导上下文。编译器在解析到这一步时,就已经拒绝后续的推导了,后面我们关于反推的分析实际都没有发生。
