添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
C++中如何检测类成员变量是否存在

C++中如何检测类成员变量是否存在

声明

本篇文章完全是基于 More C++ Idioms/Member Detector 做的翻译,以及加上我自己菜鸡级别的一些理解,方便大家能轻松快乐地体验C++模板元编程的魅力。

目标

希望有某种方法,检测某个类中是否存在成员变量 X

More C++ Idioms/Member Detector 给的代码很短,我就直接贴上来了:

template<typename T>
class DetectX
    struct Fallback { int X; }; // add member name "X"
    struct Derived : T, Fallback { };
    template<typename U, U> struct Check;
    typedef char ArrayOfOne[1];  // typedef for an array of size one.
    typedef char ArrayOfTwo[2];  // typedef for an array of size two.
    template<typename U> 
    static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);
    template<typename U> 
    static ArrayOfTwo & func(...);
  public:
    typedef DetectX type;
    enum { IsMemberExist = sizeof(func<Derived>(0)) == 2 };

使用的时候也很简单:

class ClassWithX
public:
    float X = 0.f;
class ClassNoX
public:
    int Y;
int main()
    if (DetectX<ClassNoX>::IsMemberExist)
        cout << "With X" << endl;
        cout << "Without X" << endl;

如此就可以检测出类是否含有X。

读者可以先尝试自己看看代码

代码很短,会给人一种应该很简单的错觉。有兴趣的读者可以先花上五到十分钟自己尝试理解一下它。

if(YouHaveUnderstood)
    点赞();
    拜拜了您嘞();
    return;

C++中的SFINAE

在我们分析代码之前,我得先科普一下模板元编程中常用到的一个C++的特性:SFINAE(Subsititution Failure Is Not An Error 替换失败并非错误)。

啥意思呢?简单来说,假设你有两个重载「泛型函数」,并且它们对于泛型的要求不一样。

当你使用这个函数时,C++会自动挑选「能够编译通过的」那个版本的重载函数进行实例化,并最终调用它。

比如(参考XX百科):

struct Test {
  typedef int foo;
template <typename T>
void f(typename T::foo) {}  // Definition #1
template <typename T>
void f(T) {}  // Definition #2
int main() {
  f<Test>(10);  // Call #1.
  f<int>(10);   // Call #2. 并无编译错误(即使没有 int::foo)
                // thanks to SFINAE.

在上面这个例子的main函数中:

  • 由于Test有定义type foo ,所以匹配了第一个重载函数
  • 而int没有定义 foo ,所以匹配第二个重载函数

逐行分析代码

template<typename T>
class DetectX
    struct Fallback { int X; }; // add member name "X"
    struct Derived : T, Fallback { };
    template<typename U, U> struct Check;
    typedef char ArrayOfOne[1];  // typedef for an array of size one.
    typedef char ArrayOfTwo[2];  // typedef for an array of size two.
    template<typename U> 
    static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);
    template<typename U> 
    static ArrayOfTwo & func(...);
  public:
    typedef DetectX type;
    enum { IsMemberExist = sizeof(func<Derived>(0)) == 2 };

定义Fallback类和实现继承关系

首先我们要定义一个类 Fallback ,这个类里面得有一个成员变量,这个成员变量的名字一定要和我们的目标检测变量「同名」,在这个例子中也就是 X

然后就是定义一个派生类 Derived ,它会多继承于你要检测的类型 T ,以及上面我们刚定义的 Fallback 类。

定义Check结构体

template<typename U, U> struct Check;

这里有两个泛型参数,第一个由 typename 修饰,代表U是一个类型;第二个参数没有 typename 修饰,代表这是一个U类型的实例。

也就是说:

Check<int, 1> c1;   // ok

定义两种数组类型

typedef char ArrayOfOne[1];  // typedef for an array of size one.
typedef char ArrayOfTwo[2];  // typedef for an array of size two.

这个没啥,就是用 typedef 把长度为1和2的char数组分别起了个别名。

定义func函数

接下来我们要定义两个重载的模板函数。

第一个长这样:

template<typename U> 
static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);

这个 func 函数中:

  • 它得是static的
  • 返回值类型为 ArrayOfOne & ,也就是上面定义的长度为一的char数组
  • 参数类型是一个Check结构体的「指针」

Check 结构体的模板参数必须符合条件:

第一个模板参数是一个 typename::* 形式的表达式,这种表达式表示的是「某某类的某某类型成员变量」的指针。

所以 int Fallback::* 的意思就是一个指针,指向 Fallback 类中的一个 int 类型的成员变量。

第二个模板参数是 &U::X ,意思是取 U 中「名为 X 的」成员变量的引用。想要对一个类成员变量取值,这个类得是:

  1. 首先你这个类肯定得有成员变量 X ,这个没得商量
  2. 其次,这个成员函数来源于何处呢?这个也有及其严格的要求。假设类多继承于两个不同的基类A和B,并且A和B中都定义了成员变量 X ,那么这样取值就会有二义性。而 Check 构造体又强行要求第二个模板参数一定要是第一个类「实例」,也就是 X 必须是 int Fallback::X 类型的 X ,于是当有二义性时便会无法编译通过。

第二个重载函数就比较简单:

template<typename U> 
static ArrayOfTwo & func(...);

这个函数可以接受任意的参数,宽容度非常高。

我们前面提到过 SFINAE ,也就是说一旦第一个版本的 func 没有办法匹配成功了,就会去尝试匹配第二个 func

定义enum值

好了,接下来看重中之重

enum { IsMemberExist = sizeof(func<Derived>(0)) == 2 };

在这里,上面我们定义的 Derived 类作为模板参数传给了func,并传入了0作为参数。

为啥要传入0作为参数呢?因为第一个函数参数是一个指针类型,可以传入一个0。其实也可以传入一个 NULL

前文有说过, Derived 继承于两个类: Fallback 和我们要验证的类 T

假设我们有一个类要验证如下:

class ClassWithX
    char X;
int main()
    bool bWithX = DetectX<ClassWithX>::HasMember;