SFINAE概念的介绍
引入
前段时间看 std::unique_ptr
的实现,发现里面用到了好多用到std::enable_if
的地方,查了下才知道这涉及到C++ 里 SFINAE 的规则,还有定义成员类型的时候也在注释里提到了利用 SFINAE ,所以特地查阅资料记录下。
SFINAE 定义
SFINAE 表示 Substitution Failure Is Not An Error (替换失败不是错误)。这里的 Substitution (替换)是个什么概念呢?
先来了解一下模板实参替换的概念:
已指定、推导出或从默认模板实参获得所有模板实参时,函数参数列表中每次模板形参的使用都会被替换成对应的模板实参。
替换发生于:
-所有用于函数类型中的类型(包含返回类型和所有参数的类型)
-所有用于模板形参声明中的所有类型
-所有用于函数类型中的表达式
-所有用于模板形参声明中的表达式
(ps:后两种替换都是从C++11起)
听起来很拗口,总之就是模板形参会被替换成实参。
举个简单的例子就能懂了:
1 | template<typename T, |
粗略的了解了替换之后,就能明白 SFINAE 大概什么意思了:模板形参替换推导类型失败时,从重载集抛弃特化,而非导致编译失败。其实还是挺好理解的,举个例子:
1 | struct test{ |
对于调用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 | // Primary template. |
根据 SFINAE 规则,std::enable_if<true, someType>
的时候,它提供了成员类型 type
; std::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 | template<typename T> |
有了 std::enable_if
,再配合 type_traits 头文件提供的用于判断两种类型是否能转化的函数 std::is_convertible
,完成这样的需求就很容易了:
1 | template<typename T> |
当 U*
能够转化成 T*
的时候,std::is_convertible<U*, T*>::value
为 true
,std::enable_if<std::is_convertible<U*, T*>::value>::type
为void,模板参数能正常推断;反之,当 U*
不能够转化成 T*
的时候,std::is_convertible<U*, T*>::value
为 false
,std::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 | template<typename T, typename Deleter = default_delete<T>> |
如果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*
。