跳转到内容

什么是SFINAE?

SFINAE 是 substitution failure is not an error (替换失败不是错误)的缩写

SFINAE 是一种 paradigm(范式)。

什么是“替换失败”(substitution failure)?

在 模板类型参数、函数参数、返回类型 这三处出现问题,叫做“替换失败”。就是下面***的位置

template <***>
*** render(***) {
// function body
}

“替换失败不是错误”,即如果发生了替换失败,编译器不会抛出错误,只是会把模板从重载解析的候选列表中删除,然后继续进行重载解析,找下一个最佳候选。

例子

render(3.14);会调用模板函数render(T object)还是普通函数render(float x)

#include <iostream>
template <typename T>
void render(T object) {
std::cout << "Template Function Called\n";
object.render();
}
void render(float x) {
std::cout << "Non-Template Function Called\n";
}
int main() {
render(3.14);
}

3.14是一个 double,鉴于模板可以生成与double参数完全匹配的函数,编译器会选择模板函数render(T object)。这将导致编译错误。但所选函数是否能够编译不是编译器进行重载解析时会考虑的因素。

可以通过在调用render()时显式将3.14转换为 float 来解决(render(float(3.14));),但这样不够优雅。

想在模板中解决该问题。如果实例化某个模板函数将导致编译错误,那么在重载解析中就不选择该模板,而是选择其他的重载。可以使用 SFINAE 范式(也可以使用 C++20 中引入的概念(concepts))。

SFINAE 范式做法:引入一个额外的模板参数或函数参数,如果这个参数不符合要求,则“替换失败”(substitution failure),则模板无效,就继续匹配其他的候选者。

#include <iostream>
template<typename T>
void render(T object,
decltype(object.render()) *y = nullptr) {
std::cout << "Template Function Called\n";
object.render();
}
void render(float x) {
std::cout << "Non-Template Function Called\n";
}
int main() {
render(3.14);
}

上面代码中添加了一个名为y的函数参数,其类型是指向 第一个参数的render()函数成员的返回类型 的指针。该参数在函数体内未使用,并且也不希望用户在调用函数时填写该参数的实参,因此将其默认值设为nullptr

object没有render()方法时,将出现一个“替换失败”(substitution failure),从而编译器会将该模板从候选范围中移除。

double参数没有render方法,所以在替换失败后,会回退到使用render(float)重载。

struct Rock {
static void render() {
std::cout << "Rendering a Rock";
}
};

Rock{}具有render()函数成员,因此模板可以正常实例化(render(Rock{});)。

使用std::enable_if完成 SFINAE

上面例子的完整代码如下:

#include <iostream>
struct Rock {
static void render() {
std::cout << "Rendering a Rock";
}
};
template<typename T>
void render(T object,
decltype(object.render()) *y = nullptr) {
std::cout << "Template Function Called\n";
object.render();
}
void render(float x) {
std::cout << "Non-Template Function Called\n";
}
int main() {
render(3.14);
render(Rock{});
}

在上面的例子中,“替换失败”的条件是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

// this will be an int
std::enable_if<true, int>::type intA{1};
// this will be a compilation error
std::enable_if<false, int>::type intB{2};
// this will be an int too
std::enable_if_t<true, int> intC{2};

在之前的文章中,曾自定义类型特征is_renderable,代码拿过来用:

template <typename>
struct is_renderable : std::false_type {};
template <>
struct is_renderable<Rock> : std::true_type {};
template <typename T>
constexpr bool is_renderable_v{
is_renderable<T>::value};

修改模板函数,将std::enable_if_t<is_renderable<T>::value, int> = 0作为“替换失败”的条件。

template <typename T, std::enable_if_t<
is_renderable<T>::value, int> = 0>
void render(T object) {
std::cout << "Template Function Called\n";
object.render();
}
void render(float x) {
std::cout << "Non-Template Function Called\n";
}

如果T不满足is_renderable特征,模板将产生“替换失败”,编译器会将其从重载解析候选名单中移除。

使用decltype(object.render()) *y = nullptr还是std::enable_if_t<is_renderable<T>::value, int> = 0?我觉得后者更优雅一点。当然更优雅的是使用 C++20 引入的 概念(concepts)。

参考