C++ 模板之SFINAE和enable_if分析

前言

       本文主要介绍C++如何实现类型萃取以及如何禁止函数编译期间根据特定的条件来选择启用或禁用特定的重载,感兴趣的同学一起学习吧。
       在《C++ type traits分析》 这篇文章中,我们讲述了type traits的实现用法和基本原理。
       本文我们讨论一下两个问题:
       如果实现is_class的type traits。
       怎么样禁止函数编译期间根据特定的条件来选择启用或禁用特定的重载。

SFINAE

       SFINAE是”Substitution failure is not an error”(替换失败不是错误),官方给的解释为:在函数模板的重载决议中:为模板形参替换推导类型失败时,从重载集抛弃特化,而非导致编译错误。
       用一句简单的话来说,C++在编译的时候,模板实例化的时候就算实例化失败也不会出现错误。
       例如我们用一个简单的例子来描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
void test(typename T::noexist t)
{
}
void test(int t)
{
}
int main()
{
test(100.1);
return 0;
}

       我们在使用test(100.1)实例化模板test的时候,并不会编译错误,只是会实例化失败,使用void test(int t)这个函数。
       所以SFINAE 的含义是:编译器在将函数模板形参替换成实参的过程中,如果针对某个函数模板产生了不合法的代码,其不会直接抛出错误信息,而是继续尝试去匹配其他可能的重载函数。
       What… 这是什么鬼?不是本来C++特性就是这样嘛?有必要讲的那么高大上嘛?不急,我们使用这个特性看一下SFINAE可以做什么事情。

isClass的实现

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
39
40
41
42
namespace SFINAE
{
template<typename T>
class IsClass
{
private:
template<typename U>
static constexpr bool is_class(int U::*)
{
return true;
}
template<typename U>
static constexpr bool is_class(...)
{
return false;
}
public:
static constexpr bool value = is_class<T>(nullptr);
};
}
class A
{
};
class B
{
int a;
int b;
};
int main()
{
std::cout << "A is class: " << SFINAE::IsClass<A>::value << std::endl;
std::cout << "B is class: " << SFINAE::IsClass<B>::value << std::endl;
std::cout << "int is class: " << SFINAE::IsClass<int>::value << std::endl;
std::cout << "double is class: " << SFINAE::IsClass<double>::value << std::endl;
return 0;
}

       这里有两个技巧:
       int U::* 使用这个来模板实例化,如果是类,可以定义类的成员指针,这个最佳匹配。
       is_class(…) 如果不是类,那么这个函数适配所有的类型。
       这个的运行结构为:

1
2
3
4
A is class: 1
B is class: 1
int is class: 0
double is class: 0

       这个就是利用模板实例化的时候,如果第一个实例化不了,会继续实例化其他而不会出错的特性。

enable_if

       enable_if解决的一个问题是:如何在函数编译期间根据特定的条件来选择启用或禁用模板实例化。加入我们存在一个Add加法操作,但是我们限定一个东西,就是Add只能使用整数,像std::string不能适配,我们可以使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//整数
template<typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
auto Add(T a, T b) -> T
{
return a + b;
}
int main()
{
std::string str = "abc";
Add(100, 200);
str + "abc";
Add(str, str);
return 0;
}

       此时编译的时候就会出错:

1
2
3
: error C2672: 'Add': no matching overloaded function found
: error C2783: 'T Add(T,T)': could not deduce template argument for '__formal'
: note: see declaration of 'Add'

       也就是说在模板实例化的时候并没有成功。enable_if 的主要作用就是当某个 condition 成立时,enable_if可以提供某种类型。
       我们看下enable_if的实现原理。

1
2
3
4
5
6
7
8
9
10
template<bool _Test, class _Ty = void>
struct enable_if
{ // type is undefined for assumed !_Test
};
template<class _Ty>
struct enable_if<true, _Ty>
{ // type is _Ty for _Test
using type = _Ty;
};

       也就是说,默认情况下enable_if是一个空的类定义,如果bool _Test 参数为TRUE的话,使用片特例模板struct enable_if,这个模板可以定义type类型。例如:

1
2
3
4
5
6
7
8
9
10
11
12
class A
{
public:
int a;
};
int main()
{
std::enable_if<std::is_class<A>::value, A>::type a;
a.a = 100;
return 0;
}

       C++库还定义了如下类型:

1
2
template<bool _Test, class _Ty = void>
using enable_if_t = typename enable_if<_Test, _Ty>::type;

       类似的东西在C++标准库中使用很多,例如std::vector,如下:

1
2
3
4
5
6
7
8
template<class _Iter,
class = enable_if_t<_Is_iterator_v<_Iter>>>
vector(_Iter _First, _Iter _Last, const _Alloc& _Al = _Alloc())
: _Mybase(_Al)
{ // construct from [_First, _Last) with optional allocator
_Adl_verify_range(_First, _Last);
_Range_construct_or_tidy(_Get_unwrapped(_First), _Get_unwrapped(_Last), _Iter_cat_t<_Iter>{});
}

文章目录
  1. 1. 前言
  2. 2. SFINAE
  3. 3. isClass的实现
  4. 4. enable_if