本文作为其他文章的前置知识,仅作简单介绍。
SFINAE (Substitution Failure Is Not An Error) 意为替换失败不是错误,是 C++ 模板元编程中最基本最常见的一个技术。
它的规则为:当模板形参在替换成显式指定的类型或推导出的类型失败时,此时并不会导致编译失败,而只会从重载集中丢弃这个特化,即仿佛这个模板从来没存在过。
下面介绍最常用的两个用法来详细解释。
1. std::enable_if
比如我们有一个函数 plus
,它接受两个形参 a
和 b
,简单地返回 a + b
。
我们可能会这么写。
template<class T>
T plus(T a, T b) {
return a + b;
}
这样写确实能满足基本使用,但是如果我们对这个类型 T
有一些特殊的限制,比如我们只想要整数类型用这个函数,浮点数等类型不允许使用,那我们就需要用上 std::enable_if
了。
template<class T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
T plus(T a, T b) {
return a + b;
}
std::enable_if
是 C++11 起的一个类模板,接受两个模板参数 template<bool B, class T = void>
当 B
为 true
时,它内部会声明一个 public
类型:using type = T;
而当 B
为 false
时,这个 type
类型就不存在。
所以在上例中,B
就是 std::is_integral<T>::value
,即 T
是否为整型。
如果 T
为整型,我们拿到 type
也就是 int
,使用一下,让它等于 0(当然怎么使用都可以)
如果 T
不为整型,这里尝试拿到 type
就只能报错了,但是注意一开始讲的 SFINAE 规则,这里不会真的报错,而只会抛弃这个模板。
所以最后虽然还是编译不过,但是报错信息是(注意 ignored)
error: no matching function for call to 'plus'
note: candidate template ignored: requirement 'std::is_integral<double>::value' was not satisfied [with T = double]
std::enable_if
的位置也不是固定的,比如可以等价改为如下的样子:
template<class T>
auto plus(T a, T b) -> typename std::enable_if<std::is_integral<T>::value, T>::type {
return a + b;
}
这同样属于在模板实例化期间报错而符合 SFINAE 规则。
2. std::void_t
std::void_t
在 C++17 起才有,不过这毫无疑问是最容易实现的标准库设施没有之一了。
template<class...>
using void_t = void;
不管给什么模板参数,都转为 void
所以这里介绍另一种需求,假如我们想判断一个类里是否有某个声明的类型 type
struct HasType { using type = double; };
struct Empty {};
template<class T, class = void>
struct CheckIfHasType {
static constexpr bool value = false;
};
template<class T>
struct CheckIfHasType<T, std::void_t<typename T::type>> {
static constexpr bool value = true;
};
static_assert(CheckIfHasType<HasType>::value);
static_assert(not CheckIfHasType<Empty>::value);
CheckIfHasType
的主模板第二参数显式指定为 void
,在下面偏特化中尝试取得 T
中的 type
:typename T::type
,如果这一步失败了,那么应用了 SFINAE 的规则,抛弃偏特化,最后选择了主模板,拿到 value
为 false
。
如果这一步成功了,std::void_t
将任何 type
转为 void
,与主模板第二参数匹配,偏特化成功,拿到 value
为 true
。
(两个都存在时会优先选择更特殊的版本应该不需要多说吧)
注意事项
有一个非常常见的需求,就是一个类已经有一个模板参数为 T
了,而我们想要根据 T
的具体情况来用 SFINAE 抛弃类内个别的成员函数。那注意此时模板声明一定要写:
template<class U = T, typename std::enable_if...>
这里必须要声明一个新模板 U
,这是因为 T
在类实例化时就已经确定了,直接用的话,这个函数不符合 SFINAE 的前提:模板实例化阶段。
所以 class U = T
就是为了让这个函数获得一个模板参数 U
,重新应用于 SFINAE 规则。