SFINAE概念的介绍

引入

前段时间看 std::unique_ptr 的实现,发现里面用到了好多用到std::enable_if 的地方,查了下才知道这涉及到C++ 里 SFINAE 的规则,还有定义成员类型的时候也在注释里提到了利用 SFINAE ,所以特地查阅资料记录下。

SFINAE 定义

SFINAE 表示 Substitution Failure Is Not An Error (替换失败不是错误)。这里的 Substitution (替换)是个什么概念呢?

先来了解一下模板实参替换的概念:

已指定、推导出或从默认模板实参获得所有模板实参时,函数参数列表中每次模板形参的使用都会被替换成对应的模板实参。

替换发生于:
-所有用于函数类型中的类型(包含返回类型和所有参数的类型)
-所有用于模板形参声明中的所有类型
-所有用于函数类型中的表达式
-所有用于模板形参声明中的表达式

(ps:后两种替换都是从C++11起)

听起来很拗口,总之就是模板形参会被替换成实参。

举个简单的例子就能懂了:

1
2
3
4
5
6
7
template<typename T,
typename U = typename T::type> // 第二种替换
T& // 第一种替换
fun(T) // 第一种替换
{
// 不替换
}

粗略的了解了替换之后,就能明白 SFINAE 大概什么意思了:模板形参替换推导类型失败时,从重载集抛弃特化,而非导致编译失败。其实还是挺好理解的,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct test{
using type = int;
};
template<typename T> // 重载版本1
void fun(typename T::type) { }

template<typename T> // 重载版本2
void fun(T) { }

int main()
{
fun<test>(0); // 调用1
fun<int>(0); // 调用2
}

对于调用1,它模板参数为test,替换就已经开始了。

  • 重载版本1,替换后它变成了类似 void fun(int)
  • 重载版本2,替换后它变成了类似 void fun(test)

两者都是在重载集中的。根据函数重载匹配规则,匹配到前者。可以看到,这里其实并没有用到 SFINAE

对于调用2,它模板参数为int

  • 重载版本1, 替换后它变成了类似 void fun(int::type),很显然,这个替换是failure(失败)。所以它从重载集被中删除。

  • 重载版本2,替换后它变成了类似 void fun(int)

所以调用版本2。

说道底,Substitution Failure 的含义就是:替换的实参写入时,带来了无效(invalid)的类型或表达式(参考上面提到的替换的四种情况)为 ill-formed 。(就像上面例子里出现的类似 int::type)。我查了下C++标准草案,发现里面并没有提到 SFINAE 的概念,只提到了替换后的类型或表达式为无效的话,模板类型推导就失败了。而且标准还特地强调了,当且仅当替换后的无效(invalid)类型和表达式是在函数类型和它的模板类型参数的立即上下文(immediate context)时,才导致类型推断失败。如果替换后的类型或者表达式会引发副作用(实例化某模板特化、生成某隐式定义的成员函数等)的话,就认为它并不是在 immediate context 中,会引发程序为 ill-formed。所以说,我猜正是因为只是模板类型推断失败,就不会引发错误(error)。

上面这段说的很拗口,我也没办法。cppreference和C++标准草案n上上说的也很拗口。而且cppreference上的描述还有很多奇怪的地方,一会而 Substitution Failure, 一会又冒出个 SFINAE error ,后来看了草案才知道 SFINAE error 就是让类型推断失败的情况。我把这段摘录下来,可以结合cppreference上的描述一起看下:

If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed, with a diagnostic required, if written using the substituted arguments. [ Note: If no diagnostic is required, the program is still ill-formed. Access checking is done as part of the substitution process. — end note ] Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure. [ Note: The substitution into types and expressions can result in effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such effects are not in the “immediate context” and can result in the program being ill-formed. — end note ]

草案上还明确规定了什么情况下会让模板参数推断失败。cppreference上也介绍了,只不过正如上面所提到的,说法改了下,变成了 SFINAE error 出现的情况。相当于是对于标准的提炼和总结。

最后再提一下,如果 SFINAE 完美工作,但最终还是匹配不到某个函数,那显然也会造成编译失败。

SFINAE 应用例子

std::enable_if

这差不多算是很经典的利用 SFINAE 的例子了。直接上源代码:

1
2
3
4
5
6
7
8
9
10
// Primary template.
/// Define a member typedef @c type only if a boolean constant is true.
template<bool, typename _Tp = void>
struct enable_if
{ };

// Partial specialization for true.
template<typename _Tp>
struct enable_if<true, _Tp>
{ typedef _Tp type; };

根据 SFINAE 规则,std::enable_if<true, someType>的时候,它提供了成员类型 typestd::enable_if<false, someType>的时候,它无成员类型 type。而且由于它有了个默认模板参数 _TP = void,所以 std::enable_if<true> 提供了成员类型 type 为 void,当然,std::enable_if<false> 仍然无类型成员 type

std::enable_if对不同类型特性提供分离的函数重载与特化的便利方法。它可用作额外的函数参数、返回类型、或类模板或函数模板形参。来看个使用它的例子。std::unique_ptr所使用的默认的删除器 std::default_delete,它有一个重载版本的构造函数标准规定是这样的:从另一 std::default_delete 构造 std::default_delete 对象。此构造函数仅若 U* 可隐式转换为 T* 才参与重载决议。大概函数原型就是:

1
2
3
4
5
6
template<typename T>
default_delete{
template<class U>
default_delete( const default_delete<U>& d ) noexcept; //仅若 U* 可隐式转换为 T* 才参与重载决议。T是 default_delete 能删除的类型
// ...
}

有了 std::enable_if,再配合 type_traits 头文件提供的用于判断两种类型是否能转化的函数 std::is_convertible,完成这样的需求就很容易了:

1
2
3
4
5
6
7
template<typename T>
default_delete {
template<typename U, typename =
typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
default_delete(const default_delete<Up>&) noexcept { }
// ...
}

U* 能够转化成 T* 的时候,std::is_convertible<U*, T*>::valuetruestd::enable_if<std::is_convertible<U*, T*>::value>::type 为void,模板参数能正常推断;反之,当 U* 不能够转化成 T* 的时候,std::is_convertible<U*, T*>::valuefalsestd::enable_if<std::is_convertible<U*, T*>::value>::type 为 ill-formed 。模板参数推断失败,显然无法实例化这个函数,也无法加入重载集。达到了 仅若 U 可隐式转换为 T* 才参与重载决议* 的要求。

确定 std::unique_ptr 成员类型 pointer

这个也是阅读std::unique_ptr源码学习到的。感觉这个用法也很巧妙。首先需求是这样的,std::unique_ptr 有一个成员类型 pointer,若该类型存在则为 std::remove_reference<Deleter>::type::pointer (其中 Deleter 是unique_ptr 的第二个模板参数,用于表示删除器的类型),否则为 T* 。必须满足可空指针 (NullablePointer) 。

是这么实现的:

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, typename Deleter = default_delete<T>>
class unique_ptr
{
private:
// Use SINFAE to determine whether std::remove_reference<Deleter>::type::pointer exits
class _Pointer
{
private:
using _Del = typename std::remove_reference<Deleter>::type;

template<typename U>
static typename U::pointer __test(typename U::pointer*); // 重载版本1

template<typename U>
static T* __test(...); // 重载版本2

public:
using type = decltype(__test<_Del>(0));
};
// ...
public:
typedef _pointer::type pointer;
// ...
};

如果std::remove_reference<Deleter>::type有类型成员 pointer__test<_Del>(0)优先调用重载版本1,从而 decltype(__test<_Del>(0)) 为重载版本1的返回值,即std::remove_reference<Deleter>::type::pointer;否则,只能调用重载版本2,decltype(__test<_Del>(0)) 为重载版本2的返回值,即T*