SFINAE
一个基本的重载协议(SFINAE最初思想来源)
SFINAE全称为:替换失败非错(substituion failure is not an error)
我们已经看了不少的模板特化,那么当一个函数名和某个函数模板名称匹配后,他的重载协议大致如此:
- 根据名称找出所有适用的函数和函数模板
- 对于适用的函数模板,要根据实际情况对模板形参进行替换;替换过程中如果发生错误,这个模板会被丢弃
- 在上面两步生成的可行函数集合中,编译器会寻找一个最佳匹配,产生对该函数的调用
- 如果没有找到最佳匹配,或者找到多个匹配程度相当的函数,则编译器需要报错
在这儿,体现的是 SFINAE 设计的最初用法:如果模板实例化中发生了失败,没有理由编译就此出错终止,因为还是可能有其他可用的函数重载的。
编译期成员检测(有效/无效)
当然在后面人们发现了SFINAE可以有其他用处,如下:
1 2 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
模板,我们就可以这么写:
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 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
的替代有吗,当然也有,是一种叫做标签分发的技术:
1 2 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
的通用惯用法,你还是需要了解一下。