什么是 SFINAE?

什么是 SFINAE?

SFINAE 是 “Substitution Failure Is Not An Error” 的缩写。这是 C++ 模板元编程中的一个重要概念,它允许在模板类型推导过程中,如果某个替换失败,这个失败不会立即导致编译错误。而是使得编译器简单地放弃这次替换,并尝试其他重载或模板特化。

SFINAE 的用途

SFINAE 最常见的用途包括:

  • 编写类型特征:检查类型是否具有某个成员函数、成员类型或操作符。
  • 函数模板重载决策:在多个函数模板重载之间选择最适合的一个。
  • 类模板特化:在多个类模板特化之间选择最适合的一个。

如何使用 SFINAE

在 C++11 及其之后的版本中,SFINAE 最常见的实现方式是使用 decltypestd::enable_if 和类型特征(如 std::is_integral 等)。下面是一些使用 SFINAE 的例子:

例子 1:使用 std::enable_if 禁用某些函数重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <type_traits>
#include <iostream>

template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
foo(T t) {
std::cout << "Integral version: " << t << std::endl;
return t;
}

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
foo(T t) {
std::cout << "Floating point version: " << t << std::endl;
return t;
}

int main() {
foo(10); // Calls the integral version
foo(3.14); // Calls the floating point version
}

在这个例子中,foo 函数模板有两个重载。通过 std::enable_if,我们能够让编译器在模板类型是整型时选择第一个重载,在模板类型是浮点型时选择第二个重载。

例子 2:检查类型是否有成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <type_traits>
#include <iostream>

template<typename, typename T>
struct has_serialize {

};


template<typename C, typename Ret, typename... Args>
struct has_serialize<C, Ret(Args...)> {
private:
// 如果C类型中,包含serialize函数,并且参数符合Args...,则匹配这条规则
template<typename T>
static constexpr auto check(T*)
-> typename std::is_same<
decltype(std::declval<T>().serialize(std::declval<Args>()...)),
Ret
>::type; // 当返回类型为auto时,在参数后通过->来指定返回类型

// 否则匹配这条规则
template<typename>
static constexpr std::false_type check(...);

// 得到C类型check后的结果类型,false_type or true_type
typedef decltype(check<C>(0)) type;

public:
static constexpr bool value = type::value;
};

struct MyType {
void serialize(int) {}
};

int main() {
std::cout << has_serialize<MyType, void(int)>::value << std::endl;
}

在这个例子中,我们使用模板特化和 decltype 来检查 MyType 是否有一个接受 int 参数的 serialize 成员函数,decltype检测函数返回类型时,函数只需声明即可,无需实现。

C++17 中的 SFINAE 友好的特性

C++17 引入了 if constexprauto 类型推导的新特性,让 SFINAE 使用起来更为方便。

例子 3:使用 if constexpr

1
2
3
4
5
6
7
8
9
10
template<typename T>
auto foo(T t) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integral version: " << t << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Floating point version: " << t << std::endl;
} else {
static_assert(false, "Unsupported type");
}
}

这个例子演示了如何在函数模板内部使用 `

if constexpr` 来在编译时选择执行路径,这样可以在不生成错误的情况下根据类型条件编译代码。

小结

SFINAE 是 C++ 模板编程中一项强大的技术,可以用来在编译时根据类型特性来选择不同的代码路径。虽然 SFINAE 可以使代码变得非常灵活,但它也可能使得代码难以阅读和维护。随着 C++ 标准的发展,引入了更简洁的特性如 if constexpr 和概念(C++20 中的 concepts),它们可以使得之前需要 SFINAE 来实现的功能变得更简单和直观。