SFINAE是C++里面的术语,确切名称叫做Substitution failure is not an error
。其实准确的来说还应该加入一个前缀template
,其意义从其名称中就可以看出来:模板参数替代失败并非错误。SFINAE是一种编程技巧,利用了函数模板的重载决议(overload resolution
):当有多个同名模板函数可用时,单独的一个模板函数匹配失败并不意味着错误,只有当所有的匹配都失败的时候才是错误。只用文字描述的话,SFINAE不是很好理解,后面将给出具体事例及代码说明。
利用SFINAE实现内部类型判断
下面给出利用SFINAE实现编译期内部类型判断的最简单的例子:
#include <iostream> using namespace std; struct Test { typedef int foo; }; template <typename T> void f(typename T::foo a) { cout << "with T::foo" << endl; } // Definition #1 template <typename T> void f(T a) { cout << "without T::foo" << endl; } // Definition #2 int main() { f<Test>(10); // Call #1. cout with T::foo f<int>(10); // Call #2. cout without T::foo Without error (even though there is no int::foo) thanks to SFINAE. }
模板函数f
有两个定义,第一个定义要求类型struct Test
中定义了一个类型foo
,第二个定义则对参数类型不做任何要求。所以当我们显示实例化f<Test>
时,第一个定义匹配成功,调用第一个定义;而当显示实例化f<int>
时,第一个定义匹配失败,尝试匹配第二个定义,并最终调用第二个定义。事实上,这两个定义的先后顺序并不影响最后的结果。上面的代码的主要功能就是通过SFINAE,判断模板参数的类型是否内部定义了一个foo
类型。类似的,我们可以通过下面的代码来判断传入参数类型是否内部定义了foobar
类型:
#include <iostream> template <typename T> struct has_typedef_foobar { // Types "yes" and "no" are guaranteed to have different sizes, // specifically sizeof(yes) == 1 and sizeof(no) == 2. typedef char yes[1]; typedef char no[10]; template <typename C> static yes& test(typename C::foobar*); template <typename> static no& test(...); // If the "sizeof" of the result of calling test<T>(0) would be equal to sizeof(yes), // the first overload worked and T has a nested type named foobar. static const bool value = sizeof(test<T>(0)) == sizeof(yes); }; struct foo { typedef float foobar; }; int main() { std::cout << std::boolalpha; std::cout << has_typedef_foobar<int>::value << std::endl; std::cout << has_typedef_foobar<foo>::value << std::endl; }
如果类型T
内部定义了类型foobar
,则has_typedef_foobar<int>::value
的值为true
,否则为false
。这里之所以将no
定义为10个char
,是因为在不同的平台上可能有不同的内存对齐要求。如果定义为3个的话,遇到4字节对齐的硬件结构会导致sizeof(yes)
与sizeof(no)
都返回4,在8字节对齐的机器上也是同理。目前来说还没见过大于8字节的对齐(当然自己犯贱#pragma_pack(16)的除外),所以定义为10基本可以保证这两个类型返回的字节大小是不一样的。
用SFINAE实现类内部函数判断
SFINAE的功能不仅仅是能编译期确定类型内部是否定义了新类型,而且还能在编译期确定实参类型内部是否定义了某个成员函数,样例代码如下(注意 VS编译不过。。。):
#include <type_traits> #include <utility> template <typename T, typename = void> struct has_f : std::false_type { }; template <typename T> struct has_f<T, decltype(std::declval<T>().f(), void())> : std::true_type { }; template <typename T, typename = typename std::enable_if<has_f<T>::value>::type> struct A { }; struct B { void f(); }; struct C { }; template class A<B>; // compiles template class A<C>; // error: no type named ‘type’ // in ‘struct std::enable_if<false, void>’
首先来理解一下std::declval
,这是在C++11中引入的一个函数模板,具体定义如下:
template< class T > typename std::add_rvalue_reference<T>::type declval();
其具体作用就是返回任意类型T
的一个右值引用,即使该类型不存在构造函数。但是该右值引用不可以用来求值,只能用在不可求值环境,只能用来推导类型,如其成员变量和成员函数的类型。而decltype(std::declval<T>().f(), void())
的作用就是强制进行std::declval<T>().f()
的类型推导,并最后返回void
类型。
std::false_type,std::true_type
内部都有一个成员value
,类似于我们上个事例代码中的has_typedef_foobar<int>::value
,值分别为false,true
。而std::enable_if
是一个类模板,并显示特化为了true,false
两种类型。std::enable_if<true>
内部定义了一个新类型type
,而std::enable_if<false>
内部则没有定义这个类型。std::enable_if
具体定义代码如下所示:
template<bool B, class T = void> struct enable_if {}; template<class T> struct enable_if<true, T> { typedef T type; };
在这一系列的模板特化和实例化之下,语句A<B>
能够编译通过,而语句A(C)
则编译报错。
当前我们只做到了判断没有参数也没有返回值的函数的存在性,事实上还可以判断具体签名的成员函数的存在性。假设我们要判断某个类内部是否定义了size_t used_memory() const
这个函数,该需求可以通过下面的代码来实现:
template<typename T> struct HasUsedMemoryMethod { template<typename U, size_t (U::*)() const> struct SFINAE {}; template<typename U> static char Test(SFINAE<U, &U::used_memory>*); template<typename U> static int Test(...); static const bool Has = sizeof(Test<T>(0)) == sizeof(char); }; template<typename TMap> void ReportMemUsage(const TMap& m, std::true_type) { // We may call used_memory() on m here. } template<typename TMap> void ReportMemUsage(const TMap&, std::false_type) { } template<typename TMap> void ReportMemUsage(const TMap& m) { ReportMemUsage(m, std::integral_constant<bool, HasUsedMemoryMethod<TMap>::Has>()); }
这里用到了成员指针::*
这个类型,因为普通指针无法指向成员函数(多了一个this指针的参数),具体细节读者自行百度吧。由于个人能力所限,还无法得到有参数的成员函数的存在性判断,但是现在最起码得到了有返回值类型的成员函数的存在性判断,先偷着乐一会吧。
利用SFINAE实现指针类型判断
对于一个变量,我们还可以利用SFINAE来判断该变量是否是指针类型的:包括普通变量指针,成员变量指针,成员函数指针,函数指针这四种指针类型。type_traits
中的is_pointer
元函数可以做到这一功能,代码如下:
template <typename T> struct is_pointer { template <typename U> static char is_ptr(U *); template <typename X, typename Y> static char is_ptr(Y X::*); template <class U> static char is_ptr(U (*)()); static double is_ptr(...); static T t; enum { value = sizeof(is_ptr(t)) == sizeof(char) }; }; struct Foo { int bar; }; int main(void) { typedef int * IntPtr; typedef int Foo::* FooMemberPtr; typedef int (*FuncPtr)(); printf("%d\n",is_pointer<IntPtr>::value); // prints 1 printf("%d\n",is_pointer<FooMemberPtr>::value); // prints 1 printf("%d\n",is_pointer<FuncPtr>::value); // prints 1 return 0; }
上面的代码通过显示特化了所有的指针类型并让函数模板返回值为char
,同时让非指针类型的函数模板特化为返回double
。这两个返回类型在sizeof
运算符下返回的值是不同的,所以我们可以把指针类型与非指针类型区分出来。
利用SFINAE实现类继承判断
除了上面提到的类内部类型和类函数存在性判断之外,我们还可以判断某个类A
是否是从B
继承下来的。事例代码如下:
template<class A, class B> class IsDerivedFrom { private: class Yes { char a[1]; }; class No { char a[10]; }; static Yes Test( B* ); // undefined static No Test( ... ); // undefined public: enum { Is = sizeof(Test(static_cast<A*>(0))) == sizeof(Yes) ? 1 : 0 }; };
static_cast<D*>(0)
这一句的作用是将0转换为一个A
类型的指针。由于Test(B*)
接受的是B*
类型的参数,所以会默认进行A*
到B*
的转换,如果可以转换,则匹配第一个模板。如果A*
没有到B*
的转换,则Test(B*)
这个模板匹配失败,并最终匹配了第二个模板。剩下的工作就好理解了,这里就不再赘述。需要注意的是,这里并不是去做真正的类型转换,而且,只有enum
和const static
类型的值才能在编译期动态赋值。
上面的代码只能实现可转换的判断,如果A
与B
的类型相同的时候,会返回1。如果我们要判断的是A
是否是B
的继承类型的时候,该结果并不令人满意。所以我们还需要增加一个代码片段,来排除相同类型的情况。代码示例如下:
template<class T> class IsDerivedFrom<T,T> { public: enum { Is = 0 }; };
上面的两个代码合并起来,就能完美的判断是否是继承类型的问题。
利用SFINAE实现纯虚类判断
我们也可以利用SFINAE来判断一个类是不是纯虚类。此时我们使用到了纯虚类的一个性质:不能声明纯虚类的变量。所以对于纯虚类A
来说,A(*)[1]
是无法实例化的。因为这个类型是一个指向A
类型的数组的指针(注意不是A
指针的数组),而数组声明要求类型不能为reference,void,function,abstract。所以我们可以用以下的代码测试类型是不是纯虚类:
template <typename T> struct IsAbstract { typedef char SmallType; typedef int LargeType; template <typename U> static char Test(U(*)[1]); template <typename U> static int Test(...); const static bool Result = sizeof(Test<T>(NULL)) == sizeof(LargeType); };
但是,正如前文所说,reference,void,function都会导致类型被判断为纯虚类,所以应该再对这几种类型做特化,这里就不写了。
利用SFINAE实现类模板特化
虽然之前的两个例子让我们感到了模板与SFINAE的强大与恐怖之处,但是得到了判断结果又能怎么样呢,好像都没有多大实际作用的样子。这里,我们给出一个有实际作用的例子:利用SFINAE实现类模板特化。
假设我们当前要实现一个容器C<T>
,用来存储T
类型的值。一个最简单的实现如下:
template <typename T> class C { private: T t; public: C(const C& rhs); C(C&& rhs) // other stuff };
由于有些类型的T
是不可拷贝的,例如std::mutex,std::unique_ptr
。但是对于这些类型的T
,is_copy_constructible<C<T>\,>::value
仍然是true
的。为了防止这些类型的拷贝构造,我们要把C<T>
的拷贝构造函数禁用。代码如下:
template<bool copyable> struct copyable_characteristic { }; template<> struct copyable_characteristic<false> { copyable_characteristic() = default; copyable_characteristic(const copyable_characteristic&) = delete; }; template <typename T> class C : copyable_characteristic<std::is_copy_constructible<T>::value> { public: C(const C&) = default; C(C&& rhs); // other stuff };
因为默认的拷贝构造函数会首先调用父类的拷贝构造函数。当父类的拷贝构造函数为delete
的时候,子类的拷贝构造函数也就相当于声明为了delete
,这个解决方案简直完美。
利用SFINAE实现函数模板特化
模板参数既能决定类的行为,同时也能决定函数的行为。如下例:
template<int N> struct A { public: int sum() const { return _sum<N - 1>(); } template <int otherN, typename = typename std::enable_if<otherN >= N>::type> explicit A(A<otherN> const &) { } A() = default; private: int _data[N]; template<int I> typename std::enable_if< I, int>::type _sum() const { return _sum<I - 1>() + _data[I]; } template<int I> typename std::enable_if<!I, int>::type _sum() const { return _data[I]; } }; int main() { A<4> a4; A<5> a5; A<3> a3(a5); A<7> a7(a3); //error return 1; }
上面的代码中,_sum<N>
是一个模板元函数,A<N>
是一个封装了N个整数的结构体。_sum<N>
非常巧妙的利用了int
与bool
的隐式类型转换,以及std::enable_if
的特性,还有类型别名机制,实现了递归求前N个元素和的壮举。同时,A(A<otherN> const)
也通过利用std::enable_if
实现了把拷贝构造中长度控制的奇迹。看到这两个简短而又不知所云的代码是不是感觉有点怕!当SFINAE和TYPETRIATS一起结合的时候,the real terror awaits you
。
参考链接
-
wikipedia上关于SFINAE的页面 http://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error
-
stackoverflow上关于禁止某些类型的拷贝构造函数的问题链接http://stackoverflow.com/questions/27073082/conditionally-disabling-a-copy-constructor
-
stackoverflow上关于判断类内部是否存在给定签名的成员函数的问题链接http://stackoverflow.com/questions/87372/check-if-a-class-has-a-member-function-of-a-given-signature
-
关于C++11中SFINAE使用的ppt总结http://accu.org/content/conf2013/Jonathan_Wakely_sfinae.pdf
-
cplusplus上关于enable_if的参考文档http://www.cplusplus.com/reference/type_traits/enable_if/
-
gotw上关于判断两个类是否是继承关系的讨论http://www.gotw.ca/gotw/071.htm