添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

[C++]编译时判断是否存在指定名字的函数/成员

相比于类型判断,判断某一个对象是否拥有某一个成员的情况不太常见。不过有时候吧,我们需要更稳健地使用模板可能会使用到这个功能。举个简单的例子:

template<class B>
class A
  int Do() { return B::what; }

显然对于模板对象A的Do函数而言,我们的模板对象B必须拥有名为what的成员否则编译必定导致失败。更进一步,如果我们需要在出现这种情况时,能够进行一些分支判断而不是直接给出错误的话,我们就需要在编译器判断模板对象B是否存在what这个成员了。

那么这个问题的需求就具体为:
在编译器判断指定名字的成员是否存在;同时给出一个布尔值保存判断结果而非直接编译错误;

C++20可以使用新特性Concept来处理,但对于我这种常年徘徊于C++14/C++17的选手就只能另谋出路了。所幸我们有SFINAE特性,于是第一个能够支持C++14/C++17的版本就出现了:

#include <utility>
#include <iostream>
template<typename which, typename = void>
struct HasNameOf : std::false_type {};
template<typename which>
struct HasNameOf<which, std::void_t<decltype(&which::What)>> : std::true_type {};
struct TestYes { void What(); }
struct TestNo  {}
int main()
  std::cout << HasNameOf<TestYes>::value << std::endl;
  std::cout << HasNameOf<TestNo> ::value << std::endl;

这里我们的例子是对成员函数(方便后续的介绍)进行判断,对于成员变量则可以结合std::declval来实现。这里利用了模板偏特化的SFINAE特性,当我们对TestYes进行推导时,由于What函数存在因此会选择true_type的分支;而TestNo则会选择false_type的分支。

进一步,尝试复用HasNameOf对象:

其实基本功能到此就结束了,用一个宏定义包裹一下就可以正常使用。但是在真正使用时,如果每一个需要判断的成员都需要给一个偏特化会有点麻烦。因此我们首先尝试将判断对象和判断目标分离看看能不能少,向这样:

#include <utility>
#include <iostream>
template<typename which>
struct HasNameOf_Checker
  using type = decltype(&which::What);
template<template<typename> class checker, typename which, typename = void>
struct HasNameOf_Impl : std::false_type {};
template<template<typename> class checker, typename which>
struct HasNameOf_Impl<checker, which, std::void_t<typename checker<which>::type>> : std::true_type {};
template<template<typename> class checker, typename which>
struct HasNameOf : public HasNameOf_Impl<checker, which> {};
struct TestYes { void What(); };
struct TestNo  {};
int main()
  std::cout << HasNameOf<HasNameOf_Checker, TestYes>::value << std::endl;
  std::cout << HasNameOf<HasNameOf_Checker, TestNo> ::value << std::endl;
}

这样,我们的判断对象为HasNameOf,而判断目标由HasNameOf_Checker提供。对于不同的判断目标实现不同的HasNameOf_Checker。

嗯,看起来是可以了,但也仅限于看起来。尝试编译上述代码可以发现:它通过了TestYes的判断,但在判断TestNo时报编译错误了。显然,上面的代码并没有使得SFINAE特性生效。未能成功生效的原因是在于我们希望它能够在HasNameOf_Impl触发SFINAE,但将判断语句放在HasNameOf_Checker内部会直接导致因为TestNo不存在What,而使得HasNameOf_Checker无法成功声明。因此,如果希望它能够正确执行,我们需要保证HasNameOf_Checker无论如何都可以声明,这样对HasNameOf_Checker::type的判断才能够成功触发SFINAE。因此我们如下修改了代码:

#include <utility>
#include <iostream>
template<typename which, typename = void>
struct HasNameOf_Checker;
template<typename which>
struct HasNameOf_Checker<which, std::void_t<decltype(&which::What)>>
  using type = void;
template<template<typename, typename = void> class checker, typename which, typename = void>
struct HasNameOf_Impl : std::false_type {};
template<template<typename, typename = void> class checker, typename which>
struct HasNameOf_Impl<checker, which, std::void_t<typename checker<which>::type>> : std::true_type {};
template<template<typename, typename> class checker, typename which>
struct HasNameOf : public HasNameOf_Impl<checker, which> {};
struct TestYes { void What(); };
struct TestNo  {};
int main()
  std::cout << HasNameOf<HasNameOf_Checker, TestYes>::value << std::endl;
  std::cout << HasNameOf<HasNameOf_Checker, TestNo> ::value << std::endl;
}

这样修改后,HasNameOf_Checker总是能成功声明,但在HasNameOf_Impl对type进行判断时,由于TestNo版本未定义,因此触发SFINAE选择了false路径进行编译。

好吧,经过这样修改之后,好消息是我们成功将判断对象和判断目标分离了;坏消息是这样改还不如不改...(你看看代码哪里简化了?)

另一个方案,返回值判断

既然用模板对象判断无法达到简化的目的,就考虑其它方式了。用模板对象进行判断主要的问题是:为了保证HasNameOf_Checker能够成功声明,不得不在HasNameOf_Checker对象上又进行一次偏特化。为了避免这种再次偏特化的行为,我们考虑用模板函数的返回值来设计HasNameOf_Checker会不会更合适:

#include <utility>
#include <iostream>
struct HasNameOf_Checker
  template<typename which> inline static decltype(&which::What) Do();
template<class checker, typename which, typename = void>
struct HasNameOf_Impl : std::false_type {};
template<class checker, typename which>
struct HasNameOf_Impl<checker, which, std::void_t<decltype(checker::template Do<which>())>> : std::true_type {};
template<class checker, typename which>
struct HasNameOf : public HasNameOf_Impl<checker, which> {};
struct TestYes { void What(); };
struct TestNo  {};
int main()
  std::cout << HasNameOf<HasNameOf_Checker, TestYes>::value << std::endl;
  std::cout << HasNameOf<HasNameOf_Checker, TestNo> ::value << std::endl;
}

使用Lambda函数实现

使用函数返回值作为判断就只需要在HasNameOf_Checker中实现一个模板函数就行了,不需要写偏特化。另外我们也可以发现,HasNameOf_Checker的对象结构基本上接近于Lambda函数的对象结构(一个是用户自己实现的非模板对象HasNameOf_Checker包裹了判断函数Do;另一个则是编译器生成了一个非模板Lambda对象包裹了调用重载函数operator())。因此上面的HasNameOf_Checker可以直接使用Lambda函数代替。但是,在C++14版本下,静态常量的Lambda函数不支持在头文件下定义。因此C++14下用Lambda函数进行实现意义不大,不过当我们使用C++17或以上版本后,就可以使用Lambda函数代替HasNameOf_Checker的实现了:

#include <utility>
#include <iostream>
//C++17没有同时移除const/volitale/reference的trait类,这里自己写一个。
template<typename t> using remove_cvref_t = std::remove_reference_t<std::remove_cv_t<t>>;
template<class lambda, typename which, typename = void>
struct HasNameOf_Impl : std::false_type {};
template<class lambda, typename which>
struct HasNameOf_Impl<lambda, which, std::void_t<decltype(std::declval<lambda>()(std::declval<which>()))>> : std::true_type {};
template<class lambda, typename which>
struct HasNameOf : public HasNameOf_Impl<lambda, which> {};
struct TestYes { void What(); };
struct TestNo  {};
inline constexpr auto Lambda_Checker = []( auto && _class ) -> decltype(&remove_cvref_t<decltype(_class)>::What) {};
int main()
  std::cout << HasNameOf<decltype(Lambda_Checker), TestYes>::value << std::endl;
  std::cout << HasNameOf<decltype(Lambda_Checker), TestNo> ::value << std::endl;
}

代码基本类似于函数返回值版本,唯一要注意的就是:
decltype(&remove_cvref_t<decltype(_class)>::What)。我们的decltype(_class)得到的类型一定是某一种引用类型,我们只有获取到它本身的类型时才能成功调用What,否则无论是否拥有What函数,必定返回false。所以这里必须要移除所有包括const/volitale/reference在内的所有修饰。

获取函数的名字时存在的缺陷:

虽然大部分情况下,上面的代码还是可以正确判断的。但是至少在以下几种情况下无法正确得到结果:

1. 需要获取名字的成员是非共有成员时,返回结果必定为false;
2. 获取的成员函数存在重载或是为模板函数时,返回结果必定为false;

对于第一种情况而言,虽然在功能上不成立,但是在逻辑上还是可以成立的。这个逻辑就是,当对象存在一个不对外公开的成员时,它的名字自然也不应该对外公开。我不确定是否有什么方式可以在这种情况下hack到一个私有成员的名字,有空可以再研究研究。

对于第二种情况而言,如果你需要得到某一个存在多态的函数的名字,那么必须要给出它的确定调用方式才可以,向这样(以返回值的实现方式为例):

struct HasNameOf_Checker
private:
  template<typename which> inline static decltype(static_cast<void(which::*)(int)>(&which::What)) Do();