使用C++的classFactory来模拟JAVA的反射机制
与同事聊天的时候,他和我谈到了JAVA
的反射机制,并说C++
的很多地方比不上JAVA
,反射机制就是其中之一。聊着聊着,我们就谈到C++
能否模拟一个反射机制,不求做到完美,只要能模拟出一个小功能就可以了。
JAVA的反射
首先先介绍一下JAVA的反射机制,JAVA的反射说白了就是根据类名这个字符串来初始化一个对象,还有就是根据对象能获取得到类的方法。
根据类名字符串初始化一个对象:
1 | package example; |
这个看起来就是做了一个高级别的抽象,因为接下来demo
对象要做的something
是与demo
这个对象的类是无关的。这个用在protobuf
的反序列化当中那就威力无穷了。
想象一下这样的场景:客户端发送了一条经过
protobuf
序列化之后的消息给服务器。这个消息是经过包装的,有个消息头和消息内容,消息头包括了protobuf
序列化使用的类名,序列化之后的长度等等。消息内容就是序列化的数据。然后服务器端的JAVA
代码接受到消息之后,先读消息头,取出protobuf
序列化使用的类名,根据这个类名构造一个对象,用来将接收到的消息内容反序列化。
根据对象获取类名字符串
1 | package example; |
当然,JAVA
的反射机制远远不止如此,还可以通过newInstance()
来实例化其他对象,通过getConstructors()
调用其他类的构造函数,通过getInterfaces()
返回一个类实现的接口,getSuperclass()
能取到父类。反射有非常多的API可以拿过来用。我的同事告诉我,他用的最多的就是根据类名来初始化对象,进而写更高层次的抽象代码。我对JAVA
并不是很熟,我的主流语言是C++
,但是C++
并没有反射功能,google上搜索到了可以通过工厂模式来模拟JAVA
的反射功能,仅仅是模拟根据字符串来初始化对象,于是我自己就实现了一遍。
想法
最初我的想法是这样的:使用《代码大全》的表驱动法,在程序启动的时候,把所有的对象初始化一份实例,然后在表中放入类名的字符串和这份实例的指针的映射。当需要用到反射的地方,根据提供的类名字符串查表,将实例的指针取出来。这种方法当然是不可能的,首先把所有的对象都初始化一份实例会严重拖慢程序的速度,其次,创建那么多实例,并不是全部用到,造成大量的浪费。
所以好方法就是等到需要这个实例的时候再创建,那么办法就是在表中不存实例的指针,而是保存构造函数,等到传递类名字符串的时候,取出相应的构造函数,构造出一个对象,并返回其指针。
但是难点就在C++
如何在map
当中保存构造函数。要用C++
来做这件事情的话,应该是这个样子:
1 | //伪代码: |
保存函数指针肯定是不行的,原因:构造函数的每个参数都不同,而map
如果保存函数指针的话,需要类型一致,而且就算都传无参的构造函数,由于构造函数根本没有返回类型,不可能做出函数指针指向它。
那能不能不传构造函数的指针,而是在每一个类当中定义一个Instance()
函数,函数中实例化这个类的一个对象并返回。那在表当中就保存Instance()函数的指针?
也不行,原因是Instance()
函数是member function
,要想调用一般的member function
,除非这个member function
是static
类型的,否则就必须要用一个对象去调用。
那能不能在改一步,使用《Effective C++》当中条款4
的singleton
模式,写成这种形式:
1 | static MyClass& MyClass::Instance() { |
这样总行了吧?现在使用Instance()
函数这样用就可以了,根本就不需要先存在对象了
1 | MyClass::Instance(); |
而且为了匹配函数指针,可以这样写:
1 | //定义一个函数指针, 无参数,返回值为void* |
这个策略按照常理是可以的。但是确定要这么做吗?这样做就意味着为了模拟一下JAVA
的反射机制,在每个类的定义中都需要加上这个singleton
代码,如果类很多的话,这个工作量是很大的,虽然可以写一个批量插入的程序减少工作量,但是这样从程序设计的角度真的好吗?为了模拟一个功能而修改所有的代码?我不知道什么场景下值得这么做,但是至少我是不会这么做的。
我的最后方法是,不用上面的singleton
的member function
,而是写一个non-member function
:
1 | static void* CreateClass##class_name (){ \ |
首先要提到的是这是一个在宏当中的函数,把这个函数写到宏当中有2个目的:
- 减少代码量。
- 传递的宏参数是class_name,这个参数不需要加引号变成字符串形式。这个很重要,这意味着我可以这样用而保证合法:
IMPL_CLASS_CREATE(MyClass)
,其中MyClass
不需要加上引号,从而能直接实现new MyClass()
。如果不是宏当中的函数,或者直接说是在编译期间而不是预处理期间的话,传递不加引号的MyClass
绝对非法,如果传递了带引号的MyClass
,就说明传了一个字符串,那试问如何实现new MyClass()
?
接下来请看我的C++
实现,后面会附带讲解。
C++实现
代码比较短,我就直接贴上来了:class_factory.h
1 | #ifndef MQUEUE_INCLUDE_CLASS_FACTORY_H_ |
class_factory.cpp
1 | #include "MQueue/class_factory.h" |
有关继承自Object
Object
就是一个抽象类,设计它的原因是这样的,我开始在代码当中写的并不是
1 | typedef void* (*ObjectCreate_t)(); |
而是
1 | typedef Object* (*ObjectCreate_t)(); |
因为我最初的想法是想利用C++
的多态性质来模拟反射,当时的想法是为了能将函数指针统一起来放入factoryMap_
当中,第一想法就是使用基类的指针,根本没想到void*
,使用Object*
的话就导致实例化出来的对象只能调用virtual
函数。在最开始的时候写代码写的还很顺畅,到了后来我才碰壁了,才改成了void*
。
有关 inline ClassFactory& ClassFactoryInstance()
关于这个函数的设计有必要展开讨论。首先,既然设计出了这个factory
,需要用到它的场景就是这样子:整个代码当中只有一份factory
,功能就是生产对象。所以任何时候我将ObjectCreate_t
这个函数指针放入factory
当中的时候,这个factory
一定是唯一的。
如果比较难理解的话可以这样想:如果代码中存在两份factory
,那么我要将ObjectCreate_t
这个函数指针放入factory
当中的时候,应该放入哪一个?所以只有一份factory
就能避免这个问题。
如何在整份代码中只设置一份factory
?全局变量一定是不行的。首先就是变量名的冲突,然后也不能保证其他人要用我的代码的时候会不会设置两个全局变量的factory
。要替代全局变量的办法就是使用singleton
模式,使用ClassFactory::Instance()
的方法来调用绝对不会有变量名的冲突,而且也不可能有两个factory
。
既然决定了要使用singleton
的方式来实现,接下来有必要讨论一下用《Effective C++》当中条款4
和条款21
。
条款21说的是:必须返回对象时,别妄想返回其reference
而我的singleton
设计是这样子的,而且条款4当中Meyers自己也写了这么一个singleton
。
1 | ClassFactory& Instance() { |
是不是Meyers搞错了?别急,接着往下看,在条款21当中说到了:
请记住:绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为”在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
恍然大悟!当在单线程环境当中,如果不需要多个这样的对象,那么返回pointer或reference指向一个local static对象是可以这么做的。singleton
就是单例的设计模式,所以只要保证单线程就可以了。
那如何在C++的多线程当中保证singleton
的安全性?
人家Meyers都说了不要在多线程中使用singleton
,那就不要用了。但是如果不听他的劝告,非要用的话,用《Effective C++》当中条款4
也告诉了方法:
任何一种non-const static对象,不论它是local还是non-local,在多线程环境当中”等待某事发生”都会有麻烦,处理这个麻烦的一般做法是:在程序的单线程启动阶段(single-thread startup portion)手工调用所有的reference-returning函数,这可以消除与初始化有关的”竞速形势(race conditions)”
Meyers还是建议我们在多线程的单线程启动阶段去用。意思就是说,程序在刚刚运行,多线程还没启动的时候,用这个singleton
是没问题的。
有关RemoveObject
我没有在ClassFactory
当中定义删除的操作,主要就是为了安全。因为我觉得对于这个工厂来说,删除掉函数指针根本没有任何实际意义,胡乱把这个接口提供出去反而会造成危险。
为什么要把AddObject
设置为bool
值?
要回答这个问题,请看这个宏:
1 | #define IMPL_CLASS_CREATE(class_name) \ |
那么,当我执行这段代码的时候:
1 | IMPL_CLASS_CREATE(ParseJsonObj); |
实际上执行的是这段代码:
1 | static void* CreateClassParseJsonObj (){ \ |
这个__attribute__((unused))
是编译器的内置宏,告诉编译器如果没有用到 _ParseJsonObj_bUnused
这个参数的时候,不要抛出
1 | WARNING: warning "unused parameter xxx” |
这个警告。
那么真正的实现就是这个样子:
1 | static void* CreateClassParseJsonObj (){ \ |
请看_ParseJsonObj_Unused
这个参数,到不是说这个参数有多重要,你看我给它起的名字是unused就知道以后用不上这个参数了,但是如果AddObj
的返回值为void
,那么这里就应该是:
1 | ClassFactory::Instance().AddObject("ParseJsonObj", CreateClassParseJsonObj); |
这行代码是不合法的。因为C++
不允许在全局的作用域内调用函数。在main
函数之外调用这个宏IMPL_CLASS_CREATE(ParseJsonObj)
就是在全局作用域当中。
为了让这行代码合法应该怎么做呢?
请看:
1 | static bool _ParseJsonObj_Unused = ClassFactory::Instance().AddObject("ParseJsonObj", CreateClassParseJsonObj); |
这行代码就是初始化一个静态全局变量了,C++就判断合法。把这个_ParseJsonObj_Unused
设置为static
的目的只是为了不让其他文件访问,利用了static
变量只能在本文件中访问的特性。因为它根本没有用,就怕其他文件乱改。
为什么不能在main
函数当中使用这个宏IMPL_CLASS_CREATE(ParseJsonObj)
?这样的话ClassFactory::Instance().AddObject("ParseJsonObj", CreateClassParseJsonObj);
不就合法了吗?
不错,这样子ClassFactory::Instance().AddObject("ParseJsonObj", CreateClassParseJsonObj);
是合法了,但是
1 | static void* CreateClassParseJsonObj (){ \ |
却又不合法了。因为C++不允许在函数当中又定义函数。
所以为了能让宏合法,这里的AddObject
必须要有返回值,返回bool
值的原因是bool
只占一个字节,而int
要占四个字节。
测试代码请参见:https://github.com/adairjun/MQueue/blob/master/gtest/test_class_factory.cpp