CPP SFINAE

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*)这个模板匹配失败,并最终匹配了第二个模板。剩下的工作就好理解了,这里就不再赘述。需要注意的是,这里并不是去做真正的类型转换,而且,只有enumconst static类型的值才能在编译期动态赋值。

上面的代码只能实现可转换的判断,如果AB的类型相同的时候,会返回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。但是对于这些类型的Tis_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>非常巧妙的利用了intbool的隐式类型转换,以及std::enable_if的特性,还有类型别名机制,实现了递归求前N个元素和的壮举。同时,A(A<otherN> const)也通过利用std::enable_if实现了把拷贝构造中长度控制的奇迹。看到这两个简短而又不知所云的代码是不是感觉有点怕!当SFINAE和TYPETRIATS一起结合的时候,the real terror awaits you

参考链接

Published:
2015-04-28 20:05
Category:
Tag:
CPP15