C++ SFINAE 简单介绍与两个常用用法

Posted by Ciel's Paperplane on September 4, 2024

图片

本文作为其他文章的前置知识,仅作简单介绍。

SFINAE (Substitution Failure Is Not An Error) 意为替换失败不是错误,是 C++ 模板元编程中最基本最常见的一个技术。

它的规则为:当模板形参在替换成显式指定的类型或推导出的类型失败时,此时并不会导致编译失败,而只会从重载集中丢弃这个特化,即仿佛这个模板从来没存在过。

下面介绍最常用的两个用法来详细解释。

1. std::enable_if

比如我们有一个函数 plus,它接受两个形参 ab,简单地返回 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>

Btrue 时,它内部会声明一个 public 类型:using type = T;

而当 Bfalse 时,这个 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 中的 typetypename T::type,如果这一步失败了,那么应用了 SFINAE 的规则,抛弃偏特化,最后选择了主模板,拿到 valuefalse

如果这一步成功了,std::void_t 将任何 type 转为 void,与主模板第二参数匹配,偏特化成功,拿到 valuetrue

(两个都存在时会优先选择更特殊的版本应该不需要多说吧)

注意事项

有一个非常常见的需求,就是一个类已经有一个模板参数为 T 了,而我们想要根据 T 的具体情况来用 SFINAE 抛弃类内个别的成员函数。那注意此时模板声明一定要写:

template<class U = T, typename std::enable_if...>

这里必须要声明一个新模板 U,这是因为 T 在类实例化时就已经确定了,直接用的话,这个函数不符合 SFINAE 的前提:模板实例化阶段。

所以 class U = T 就是为了让这个函数获得一个模板参数 U,重新应用于 SFINAE 规则。



Ciel's Paperplane

作诗中...