SFINAE
一个基本的重载协议(SFINAE最初思想来源)
SFINAE全称为:替换失败非错(substituion failure is not an error)
我们已经看了不少的模板特化,那么当一个函数名和某个函数模板名称匹配后,他的重载协议大致如此:
- 根据名称找出所有适用的函数和函数模板
- 对于适用的函数模板,要根据实际情况对模板形参进行替换;替换过程中如果发生错误,这个模板会被丢弃
- 在上面两步生成的可行函数集合中,编译器会寻找一个最佳匹配,产生对该函数的调用
- 如果没有找到最佳匹配,或者找到多个匹配程度相当的函数,则编译器需要报错
在这儿,体现的是 SFINAE 设计的最初用法:如果模板实例化中发生了失败,没有理由编译就此出错终止,因为还是可能有其他可用的函数重载的。
编译期成员检测(有效/无效)
当然在后面人们发现了SFINAE可以有其他用处,如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | template<typename T>struct has_reserve
 {
 struct good{char dummy;};
 struct bad{char dummy[2];};
 
 
 template<class U, void (U::*)(size_t)>
 struct SFINAE{};
 
 
 template<class U>
 static good reserve(SFINAE<U, &U::reserve>*);
 
 
 
 
 template<class U>
 static bad reserve(...);
 
 
 
 static const bool value = sizeof(reserve<T>(nullptr)) == sizeof(good);
 };
 
 | 
上面这段代码的作用是?
C++11开始出现了一个enable_if的模板(type_traits.h),可以选择性的启用某个函数的重载:
比如我们想要向容器尾部添加元素,我们希望的原型是这样的:
|  | template <typename C, typename T>void append(C& container, T* ptr, size_t size);
 
 | 
reserver这个函数常常出现在容器里,表示给容器对象分配至少能容纳n个元素的内存。
显然,container 有没有 reserve 成员函数,是对性能有影响的——如果有的话,我们通常应该预留好内存空间,以免产生不必要的对象移动甚至拷贝操作。利用 enable_if和上面的has_reserve 模板,我们就可以这么写:
| 12
 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
 43
 44
 45
 46
 47
 
 | template<typename T>struct has_reserve
 {
 struct good{char dummy;};
 struct bad{char dummy[2];};
 
 
 template<class U, void (U::*)(size_t)>
 struct SFINAE{};
 
 
 template<class U>
 static good reserve(SFINAE<U, &U::reserve>*);
 
 
 
 
 template<class U>
 static bad reserve(...);
 
 
 
 static const bool value = sizeof(reserve<T>(nullptr)) == sizeof(good);
 };
 
 
 
 template<typename C, typename T>
 enable_if_t<has_reserve<C>::value, void>
 append(C& container, T* ptr, size_t size)
 {
 container.reserve(container.size() + size);
 for(size_t i = 0; i< size; ++i)
 {
 container.push_back(ptr[i]);
 }
 }
 
 template<typename C, typename T>
 enable_if_t<!has_reserve<C>::value, void>
 append(C& container, T* ptr, size_t size)
 {
 for(size_t i = 0; i< size; ++i)
 {
 container.push_back(ptr[i]);
 }
 }
 
 | 
- 对于某个 type trait,添加 _t 的后缀等价于其 type 成员类型。因而,我们可以用enable_if_t来取到结果的类型
- enable_if_t<has_reserve<C>::value, void>的意思可以理解成:如果类型 C 有成员的话,那我们启用下面的成员函数,它的返回类型为 void。
简单来说通过enable_if+ 我们自己写的has_reserve实现了编译期选择了合适的函数。
当时我们也发现了这类函数都是有返回值的,在返回值上用enable_if+ 我们自己写的has_reserve实现了编译期选择了合适的函数,如果没有返回值(比如构造函数)或者你不想手写返回值类型的时候该怎么办呢?
https://en.cppreference.com/w/cpp/types/enable_if  未来填坑这种解决方法。
编译期成员检测(仅有效)
如果只需要在某个操作有效的情况下启用某个函数,而不需要考虑相反的情况的话,有另外一个技巧可以用。对于上面的 append 的情况,如果我们想限制只有具有 reserve 成员函数的类可以使用这个重载,我们可以把代码简化成:
|  | template <typename C, typename T>auto append(C& container, T* ptr,size_t size)-> decltype(declval<C&>().reserve(1U),void())
 {
 container.reserve(container.size() + size);
 for (size_t i = 0; i < size; ++i)
 {
 container.push_back(ptr[i]);
 }
 }
 
 | 
declval: 这个模板用来声明一个某个类型的参数,但这个参数只是用来参加模板的匹配,不允许实际使用。使用这个模板,我们可以在某类型没有默认构造函数的情况下,假想出一个该类的对象来进行类型推导
因此declval<C&>().reserve(1U)就是在测试 C& 类型的对象是不是可以拿 1U 作为参数来调用 reserve 成员函数, 如果不可以就是替换失败(Substitution failure) 。
C++ 里的逗号表达式的意思是按顺序逐个估值,并返回最后一项。所以,上面这个函数的返回值类型是 void。
再谈一下declval<C&>().reserve(1U)这个方式和 enable_if不同,很难表示否定的条件。如果要提供一个专门给没有reserve 成员函数的 C 类型的 append 重载,这种方式就不太方便了。因而,这种方式的主要用途是避免错误的重载。因此declval<C&>().reserve(1U)替换失败后发现没有其他可替换的了直接报错,避免错误的重载。
void_t
C++17引入了void_t, 定义很简单。
|  | template <typename...>using void_t = void;
 
 | 
利用decltype、declval 和模板特化,我们可以把 has_reserve 的定义大大简化:
换句话说,这个类型模板会把任意类型映射到 void。它的特殊性在于,在这个看似无聊的过程中,编译器会检查那个任意类型的有效性。利用 decltype、declval 和模板特化,我们可以把has_reserve的定义大大简化:
|  | template <typename T,typename = void_t<>>struct has_reserve : false_type {};
 
 
 template <typename T>
 struct has_reserve<T,void_t<decltype(declval<T&>().reserve(1U))>>: true_type {};
 
 | 
当第二个模板能被满足时,编译器就会选择第二个特化的模板;而只有第二个模板不能被满足时,才会回到第一个模板的通用情况。
有了这个 has_reserve模板,我们就可以继续使用其他的技巧,如 enable_if和下面的标签分发,来对重载进行限制。
继承false_type/true_type我们可以查询has_reverse中的静态的value(来自false_type/true_type),回想我们在最开始写的那个东西中有一个has_reserve<C>::value和这个作用就等价了,而代码量小了非常多。
|  | template<typename C, typename T>enable_if_t<has_reserve<C>::value, void>
 append(C& container, T* ptr, size_t size)
 {
 container.reserve(container.size() + size);
 for(size_t i = 0; i< size; ++i)
 {
 container.push_back(ptr[i]);
 }
 }
 
 | 
标签分发
现在我们完成了has_reserve<C>::value替代.
那么enable_if的替代有吗,当然也有,是一种叫做标签分发的技术:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | template<typename C,typename T>void _append(C& container, T* ptr, size_t size, true_type)
 {
 container.reserve(container.size() + size);
 for(size_t i = 0; i < size. ++i)
 {
 container.push_back(ptr[i]);
 }
 }
 
 template<typename C,typename T>
 void _append(C& container, T* ptr, size_t size, false_type)
 {l
 for(size_t i = 0; i < size. ++i)
 {
 container.push_back(ptr[i]);
 }
 }
 
 template<typename C, typename T>
 void append(C& container, T* ptr, size_t size)
 {
 _append(container, ptr, size, integral_constant<bool,has_reserve<C>::value>{});
 }
 
 | 
如果我们用 void_t 那个版本的has_reserve 模板的话,由于模板的实例会继承false_type或 true_type 之一,代码可以进一步简化为:
|  | template <typename T,typename = void_t<>>struct has_reserve : false_type {};
 
 
 template <typename T>
 struct has_reserve<T,void_t<decltype(declval<T&>().reserve(1U))>>: true_type {};
 
 | 
|  | template<typename C, typename T>void append(C& container, T* ptr, size_t size)
 {
 _append(container, ptr, size, has_reserve<C>{});
 }
 
 | 
标签分发并没有使用 enable_if 显得方便。作为一种可以替代 enable_if的通用惯用法,你还是需要了解一下。