GCC std库实现中的一个SFINAE实例
Part 1. 源代码
在std::tuple的swap函数上有这样一个noexcept声明:
noexcept(__and_<__is_nothrow_swappable<_Elements>...>::value)
作用是检测tuple内的每一个元素是否是不抛出可交换的,这里的
这里有一个类模板__and__<T...>,用来对__is_nothrow_swappable<T>的每个value进行相互之间与运算,它的实现如下:
template<typename... _Bn>
struct __and_
: decltype(__detail::__and_fn<_Bn...>(0))
{ };
这个__and__<T...>依据函数__and_fn<_Bn...>(0)调用的返回值类型来决定继承类的类型。
而__and_fn<_Bn...>(0)的实现是这样的:
template<typename... _Bn>
auto __and_fn(int) -> __first_t<true_type,
__enable_if_t<bool(_Bn::value)>...>;
template<typename... _Bn>
auto __and_fn(...) -> false_type;
其中__first_t<T, ...>等价于T,也就是说,这个__and_fn函数的返回类型要么是std::true_type,要么是std::false_type。
看到这里你可能感到疑惑:__and_fn<_Bn...>(0)不是会选择重载__and_fn(int)吗?__and__会始终继承std::true_type吧;又或者,你甚至一眼难以看出这个函数是怎么对Bn...中的每个类型进行检测的。
Part 2. 另一个简单等效的实现
按照比较容易的想法,我们要对类型T1,T2…Tn的类成员value进行与运算,可以这样简单地实现:
template<typename ...T>
struct and_impl
{
constexpr static bool value = (T::value && ...)
};
但是这样会产生一个问题:如果类型T不含value,导致编译失败怎么办?
如果加requires,最多是让提示信息更加友好,仍然会产生编译失败的结果,并且,我们使用这个and的场合,并不总能保证用户输入预期的类型,但我们依然希望程序正常编译该如何解决?
当然,你可以为不同的类型T其添加特化,指定某个特化下的该类型应该以哪一个值参与运算:
template<typename T>
struct op_and_value
{
constexpr static bool value = T::value;
};
template<>
struct op_and_value<void>
{
constexpr static bool value = false;
};
template<typename ...T>
struct and_impl
{
constexpr static bool value = (op_and_value<T>::value && ...)
};
但是这样做的话会有大量特化代码,写起来繁琐复杂,不可行。
此时,就需要我们用到SFINAE
Part 3. 标准库的实现逻辑
首先我们先回答一个问题:__and_fn<_Bn...>(0)会始终选择重载__and_fn(int)吗?
答案是否定的,关键就在__first_t<true_type, __enable_if_t<bool(_Bn::value)>...>中的__enable_if_t(=std::enable_if)这里。
传入__enable_if_t第一个参数是_Bn::value,并将其转换为bool类型,第二个参数Tp不传入,使用缺省的void。对于bool(_Bn::value)的值,有以下几种情况:
- 如果其中一个
_Bn::value为false,那么enable_if::type不存在,这里就会发生错误,即使用enable_if<false, void>替换enable_if<_Cond, Tp>失败,发生SFINAE,从重载集中丢弃,此时__and_fn<_Bn...>(0)只会匹配到__and_fn(...),此时函数调用的返回值类型为false_type - 如果每个
_Bn::value均为true,则每一个enable_if都被替换成功,由于从__add_fn(0)的调用优先匹配__add_fn(int),此时产生的函数返回值类型为__first_t<true_type, void, void, ...>,即true_type - 如果任意一个
_Bn::value不存在,例如_Bn=void,则使用void替换_Bn失败,仍然产生SFINAE,从重载集中丢弃__add_fn(int)
这样,不管类型T是什么类型,只要这个类型含有value,那么就能正常参与逻辑与运算了;一旦不含value,也能编译通过,但逻辑与运算的结果始终为false。