C++ SFINAE 检测是否存在某个名称的成员函数

SFINAE(Substitution failure is not an error) 被用在很多模板的花式操作里,笔者使用的时候多半是为了将运行时的多态替换成编译器的静态多态。

简单版:检测一个类T是否存在某名字的成员函数

举个例子,有如下的类定义:

class SomeWhatClass {
public:
  void SomeWhatFunction(int x, double y);
};

希望能有一个静态的检测,能够实现

static_assert(HasSomeWhatFunction<SomeWhatClass>::value, "expecting true");

那么可以这样[1]:

template <typename T>
 class HasSomeWhatFunction {
   typedef char one;
   struct two { char x[2]; };
   template <typename C>
   static one test(decltype(&C::SomeWhatFunction));
   template <typename C>
   static two test(…);
 public:
   enum { value = sizeof(test(0)) == sizeof(char) };
 };

基本逻辑是如果被测试的typename T有成员函数,那么调用HasSomeWhatFunction的静态方法test<T>()时,模板替换是按照返回值类型为one(char)来生效的;反之如果不存在SomeWhatFunction的函数,那么返回类型one的模板替换失败,继续查找,匹配到万能重载(catch-all overload) 的static two test(…)里。

这种做法的可读性在模板的骚操作里算是好的,但是它无法区分SomeWhatFunction的函数签名(参数类型、返回值类型)。

普通版:检测一个类T是否存在某签名的成员函数

接上个例子拓展一下:

class SomeWhatClass {
public:
  void SomeWhatFunction(int x, double y);
};

// 希望能检测函数签名
static_assert(!HasSomeWhatFunction_Double<SomeWhatClass>::value, "not expecting");
static_assert(HasSomeWhatFunction_Int_Double<SomeWhatClass>::value, "expecting");

这个就需要更复杂一些的逻辑了:

 template <typename T>
 class HasSomeWhatFunction_Double {
   typedef char one;
   struct two { char x[2]; };
   
   template <typename C, void (C::*)(double)>
   struct Check;
   
   template <typename C>
   static one test(Check<C, &C::SomeWhatFunction> *);
   
   template <typename C>
   static two test(…);
 public:
   enum { value = sizeof(test(0)) == sizeof(char) };
 };

 template <typename T>
 class HasSomeWhatFunction_Int_Double {
   typedef char one;
   struct two { char x[2]; };
   
   template <typename C, void (C::*)(int, double)>
   struct Check;
   
   template <typename C>
   static one test(Check<C, &C::SomeWhatFunction> *);
   
   template <typename C>
   static two test(…);
 public:
   enum { value = sizeof(test(0)) == sizeof(char) };
 };

可以看到这个实现比之前的多定义了一个内部类Check。在初始化enum value值时会调用test,然后会尝试模板替换,进而想要去特化struct Check。如果失败了(不存在对应签名的函数),那么会回滚到static two test里。

这个实现其实能够覆盖许多场景了,但是还有一个奇怪的需求满足不了:如果要判断一个类的模板成员函数呢?

地狱版:检测一个类T是否存在某签名的模板成员函数

小改一下SomeWhatClass

class SomeWhatClass {
public:
  template <typename T>
  void SomeTempMemberFunc(int X, T& t);
};

如果需要检测void SomeTempMemberFunc<int, T&>是否存在,咋办? 搜了一圈,解释的最清楚的是参考[2]里面描述的,利用std::declval的解法。

template <typename T>
class HasTempMemberFunc {
  struct dummy {};
  constexpr static int int_holder {};
  
  template <typename C, typename P>
  static auto test(P& p) 
  -> decltype(std::declval<C>().SomeTempMemberFunc(int_holder, p), std::true_type());

  template <typename C, typename P>
  static std::false_type test(...);
public:
  static const bool value = std::is_same<std::true_type, 
                                         decltype(test<T, dummy>(nullptr))>::value;
};

几个知识点:

  • decltype里利用了含有逗号运算符[3]的表达式;
  • declval可以避开构造函数而使用类成员;

参考

[1] writing and using a C++ template to check for a function’s existence http://www.cplusplus.com/forum/beginner/70134/

[2] SFINAE Hell: detecting template methods https://blog.quasardb.net/2015/04/12/sfinae-hell-detecting-template-methods

[3] SFINAE示例 https://zh.cppreference.com/w/cpp/language/sfinae#.E7.A4.BA.E4.BE.8B

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据