什么是SFINAE?
SFINAE 是 substitution failure is not an error (替换失败不是错误)的缩写
SFINAE 是一种 paradigm(范式)。
什么是“替换失败”(substitution failure)?
在 模板类型参数、函数参数、返回类型 这三处出现问题,叫做“替换失败”。就是下面***
的位置
“替换失败不是错误”,即如果发生了替换失败,编译器不会抛出错误,只是会把模板从重载解析的候选列表中删除,然后继续进行重载解析,找下一个最佳候选。
例子
render(3.14);
会调用模板函数render(T object)
还是普通函数render(float x)
?
3.14
是一个 double,鉴于模板可以生成与double
参数完全匹配的函数,编译器会选择模板函数render(T object)
。这将导致编译错误。但所选函数是否能够编译不是编译器进行重载解析时会考虑的因素。
可以通过在调用render()
时显式将3.14
转换为 float 来解决(render(float(3.14));
),但这样不够优雅。
想在模板中解决该问题。如果实例化某个模板函数将导致编译错误,那么在重载解析中就不选择该模板,而是选择其他的重载。可以使用 SFINAE 范式(也可以使用 C++20 中引入的概念(concepts))。
SFINAE 范式做法:引入一个额外的模板参数或函数参数,如果这个参数不符合要求,则“替换失败”(substitution failure),则模板无效,就继续匹配其他的候选者。
上面代码中添加了一个名为y
的函数参数,其类型是指向 第一个参数的render()
函数成员的返回类型 的指针。该参数在函数体内未使用,并且也不希望用户在调用函数时填写该参数的实参,因此将其默认值设为nullptr
。
当object
没有render()
方法时,将出现一个“替换失败”(substitution failure),从而编译器会将该模板从候选范围中移除。
double
参数没有render
方法,所以在替换失败后,会回退到使用render(float)
重载。
Rock{}
具有render()
函数成员,因此模板可以正常实例化(render(Rock{});
)。
使用std::enable_if
完成 SFINAE
上面例子的完整代码如下:
在上面的例子中,“替换失败”的条件是decltype(object.render()) *y = nullptr
,不够优雅。这里使用std::enable_if
。
std::enable_if
是一个类型特征(type traits)本质上是一个struct
。接收两个模板参数,一个可以在编译时评估的布尔表达式,一个类型。如果布尔表达式为
true
,则成员type
为模板参数中指定的那个类型。如果布尔表达式为false
,则成员type
不存在,使用它会出错。可以使用
std::enable_if_t
来直接访问std::enable_if<>::type
。
在之前的文章中,曾自定义类型特征is_renderable
,代码拿过来用:
修改模板函数,将std::enable_if_t<is_renderable<T>::value, int> = 0
作为“替换失败”的条件。
如果T
不满足is_renderable
特征,模板将产生“替换失败”,编译器会将其从重载解析候选名单中移除。
使用decltype(object.render()) *y = nullptr
还是std::enable_if_t<is_renderable<T>::value, int> = 0
?我觉得后者更优雅一点。当然更优雅的是使用 C++20 引入的 概念(concepts)。