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
的」成员变量的引用。想要对一个类成员变量取值,这个类得是:
-
首先你这个类肯定得有成员变量
X
,这个没得商量 -
其次,这个成员函数来源于何处呢?这个也有及其严格的要求。假设类多继承于两个不同的基类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;