概述
要想"创造C++反射",先来看几个问题:
- C#反射是什么怎么用?你知道原理不!Java反射呢!
- 你的场景是什么,你想要的效果是什么样子?
某书在讲简单工厂、工厂方法模式时,提及了反射技术。忘记什么时候接触到控制反转思想,也趁机了解了一把反射技术。期间也向Java同事做过相关了解。而我的第一个应用场景是这样的,C++环境下构建一个微内核架构的插件管理器,希望能通过(配置文件读入的)字符串(string的类名称或别名)控制插件是否加载,能在插件管理中心实例化具体插件而不是客户端创建(new 类类型)出来。感觉工厂什么的有点弱弱的,于是无所畏惧的开始了C++反射之路。
啥是反射(动机)
要搞明白什么是反射技术,只能从支持它的Java/C#语言入手,因为妇孺皆知C++语言自身不支持反射!对于Java,支持反射是语言层面的不是框架层面,C++语言不支持,但是像Qt这样的框架却有实现它。
JAVA反射机制-百科,参考其中对反射意义及缺点的解释,其中提到通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类。这里的"需要提前硬编码",大概可以理解为"包含相关的头文件然后new操作",这应该也是网络资料中有些人所说的"静态和动态"相关词语的基本语义,即是否编译时刻确定。暂时肤浅的理解如下:所谓的静态就是所谓的需要提前硬编码(静态语言是在编译时变量的数据类型即可确定的语言);所谓的动态,Java归属于静态语言,它却有着一个非常突出的动态相关机制,Java的反射机制被视为Java为准动态语言的主要的一个关键性质,它允许程序在运行时透过反射取得任何一个已知名称的class的内部信息。在运行时刻把一个字符串的类名称转换为一个类对象,只是反射机制的重要应用之一,利用反射机制还能做更多(参见另外博客)。
类名到字符串,用字符串创建类对象,透过这一思想,我们最大的收获什么呢?因为你可能会觉得,无论是直接new还是通过反射来实例化对象,你感觉都是需要事先编译好,都是死死地…
//code scence:
//Class A/B all public inherits from ClassX
//常规方法
ClassX *pA = new ClassA();
//反射方法
ClassX *pA = SomeReflectionMechanism.CreateInstance("ClassA");
只看上边的代码,的确都是死的,即事先编译好的。现在给一个场景,要求ClassB可以与ClassA类替换,你大概会这么写。
//常规写法
ClassX *GetConcreteProduct(const string &c_strModleName)
{
switch (c_strModleName) {
case "ClassA":
return new ClassA();
case "ClassB":
return new ClassB();
}
}
//反射写法
ClassX *GetConcreteProduct(const string &c_strModleName)
{
return SomeReflectionMechanism.CreateInstance(c_strModleName);
}
这种场景下,你发现反射的写法不再那么死了,为啥来?因为反射方法中,可以直接用变量来处理,不再涉及任何具体产品信息,也就是说可直接根据外部需要更换。这也让我们重新来理解了一句话"从某个角度上说,所有用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合"。
“变量是可以替换的"好好体会这句话,上述所谓的写死,并不是指硬代码,而是指编译时。正是由于反射机制可以利用字符串实例化对象,不同的字符串可以归结到一个字符串变量上,这就实现了将程序由编译时转为运行时。在上述基础上,将c_strModleName变量从配置文件读取,这样在上述场景下(如增加ClassC的切换),就彻底不用再修改代码啦。再回到我的项目中,我的目的也明确了"增加一个新的具体的插件后,不能修改它之外的其他任何代码”!
逐步打破骗局
202004记(之前曾经也随大流认C++反射是彻头彻尾的骗局),可能只有正八经用过Java反射大部分功能,才可真正理解"C++实现的反射技术"到底“假”在哪里?时至今日,我还有点远。可以肯定的是,若某个人一本正经的说他自己"用C++实现了反射技术”,则要么招摇撞骗,或者压根不懂什么是反射。试想若几个C++宏操作、容器、函数指针就能实现.Net框架的反射机制、Java语言的反射机制,又怎么会说C++不支持反射。虽然"C++的反射"骗人不假,但是效果却也是有的,在某些层面上还很有用,个人觉得可把"C++反射“理解为一个不与Java反射一样的专有名词。初级的"C++反射"追求的是,更好的实现降低客户端程序与产品对象的耦合,使得要更换对象时,不需要做大的改动,最好是不必改动。
“不做大的修改"官方的说法就是"关闭修改,开放扩展”,这是面向对象根本设计原则,是GOF设计模式的终极目标所在。Here我们重点关注的是创建,那就从工厂家族的设计模式开始。这里插一段项目历史,可能有助理解,插件管理器和具体插件之间关系实现的历史版本:
菜鸟负1级版
在V1.0版本中,插件之间存在包含关系、具体插件在客户端中new创建,所谓的插件平台并无对插件生命周期的管理能力。
//平台实现
void CPlateform::RegistePlugin(AbstrctPlugin *pHmi, int iLevel){}
//抽象插件
bool AbstrctPlugin::Registe2Platform(int iLevel)
{
macro_platform->RegistePlugin(this, iLevel);
}
//具体插件实现
ConcretePluginA::ConcretePluginA() : AbstractPlugin()
{
//注册到平台
Registe2Platform(iLevel);
//包含其他插件
m_pPluginB = new ConcretePluginB(); //maybe include PluginM PluginN
m_pPluginC = new ConcretePluginC(); //maybe include PluginX PluginY
}
//客户端实现 //mian()
Q_UNUSED(new ConcretePluginFirst());..
很快,问题来了,开机数据同步结束后创建第一插件A,然后在A中创建BC,B创建MN,…然后一直到全部插件创建,这使得开机耗时很长。在V1.1版本中,初涉C++反射的概念,尽管理解和使用有诸多不恰之处,但是博主实现了延时创建,即开机时只创建A,当执行跳转到B时,若B不存在则由平台负责创建它,同理在B中跳转到M时,平台也创建M。至此,插件管理平台初步具有了插件生命周期管理的功能…
工厂家族版本
博主为了实现"c++反射"也是煞费苦心,不仅搜罗了大量网络资料来看,自己也折腾了两天多。在这个纠结的过程中,都差点忘记了要用反射的初衷:
- 大话中关于工厂方法讲解的最后,提到用反射来避免分支判断问题(简单工厂)和客户端代码修改(工厂方法)问题…P76
- 在我的项目场景中,我要实现可配置的选择性加载具体插件,本质是通过配置中的字符串值或ID值,得到一个具体的类对象…
下边是几篇关于C++反射实现的不错的文章,C++反射+简单工厂模式、宏定义的C++反射,函数模板+工厂方法、Qt框架下的C++反射,它们都给了我不少的启发,在此一并表示感谢!
简单工厂版本
#include "plugina/b/n.h" //Expose concrete class to factory
//static func in class SimpleFactory
AbstrctPlugin* SimpleFactory::CreateConcretePlugin(string type)
{
AbstrctPlugin *pConcretePlugin = NULL;
switch (type)
{
case "Plugin_A":
pConcretePlugin = new Plugin_A(); break;
case "Plugin_B":
pConcretePlugin = new Plugin_B(); break;
case "Plugin_N": .. ; break;
}
return pConcretePlugin;
}
//保留负1级版的平台实现和抽象插件
//工厂调用 //平台实现
if (m_hashPlugins.end() == m_hashPlugins.find("DES_PLUGIN_NAME"))
{
AbstractPlugin *pDesPlugin = (AbstractPlugin*)CreateFuncInClient("DES_PLUGIN_NAME");
}
//else return m_hashPlugins["DES_PLUGIN_NAME"];
因为简单工厂模式,每次扩展(插件种类)都要修改工厂类,这是对修改开放,严重的不符合开放封闭原则,所以它在GOF设计模式中连一个名分都没有。我们倒是可以将上述简单工厂的创建方法静态化然后注入到平台中用以回调,平台中需要某个插件时,平心而论这样的实现相比V1.0还是进步很大的。“使用配置文件…”,这句话的内层意思是,以后有任何的变动(如扩展新插件),你只需要修改配置文件,不必改客户端代码或工厂代码,显然简单工厂不可能达到这效果。
工厂方法版本
下边是V2.0版本的基本实现伪代码,在类的内部使用静态成员函数、或者是在h中声明全局函数,来创建产品,可以认为它们都是工厂方法模式的变种。工厂方法保持了简单工厂模式的优点(工厂根据客户端选择动态实例化相关类,客户端不依赖具体产品),克服了它的缺点(工厂类自身的修改不符合开闭原则)。工厂方法的缺点是由于每增加一个产品,就要增加一个产品工厂类,增加了额外的开发量,反射机制是一种解决那些不完美的一种有效手段。
//工厂方法定义 //abstractplugin.h
#define PLUGIN_REFLECTON_FUNC(class_name) \
static void *CreatePlugin_##class_name() \
{ return (new class_name); } \
//具体插件实现 //.h
class ConcretePluginA : public AbstractPlugin
{
public:
/** @brief same as
* //h static void *CreatePlugi_ConcretePluginA();
* //cpp void *CreatePlugi_ConcretePluginA() {return (new ConcretePluginA);}
**/
PLUGIN_REFLECTON_FUNC(ConcretePluginA)
};
//工厂 //定义插件创建函数指针
typedef void* (*FuncHmiCreate)();
//工厂 //创建函数注册接口
#define PLUGIN_REFLECTION_REGIST(c_strName, class_name) \
CPluginFactory::GetInstance()->RegistPluginCreateFunc(c_strName, class_name::CreateHmi_##class_name) \
//工厂 //回调插件创建
#define PLUGIN_REFLECTION_USE(c_strName) \
CPluginFactory::GetInstance()->GetPluginByName(c_strName);\
//工厂类定义
class CPluginFactory {
public:
static CPluginFactory*GetInstance();
public:
//注册HMI名称与创建函数指针
bool RegistPluginCreateFunc(const QString &c_strName, FuncHmiCreate func);
//根据HMI名称创建实例并返回
void* GetPluginByName(const QString &c_strName);
private:
QHash<QString, FuncHmiCreate> m_hashNameCreateFuncs; //插件别名+插件创建方法
};
//客户端 //invoked in main()
bool CPluginBuilder::RegistePluginCreateFunc2Plateform()
{
//两个实参的类型分别为: 字符串、类类型
bOK &= PLUGIN_REFLECTION_REGIST("插件A", ConcretePluginA);
bOK &= PLUGIN_REFLECTION_REGIST("插件B", ConcretePluginB);
}
//在平台内部使用函数
AbstractPlugin *pDesPlugin = (AbstractPlugin*)PLUGIN_REFLECTION_USE("DES_PLUGIN_NAME");
从小范围视角看,工厂系列模式集中封装了对象的创建,使得更换对象时,不需要做大的修改就能实现,能使客户端代码更加简洁和稳定。但是从一个更上层的视角来看,尤其是当产品管理中心与具体产品不在同一个动态库中时,有点尴尬。下边细说下这种尴尬:
- 这种尴尬在简单工厂模式时更强烈。尽管从代码执行流程上,创建过程已是由平台发起,此时该工厂模式的客户端是调用它的平台,优点(对客户端来说,去除了与具体产品的依赖)。平台独立成库,插件独立成库,工厂在它们之外,从这个层面说,这里的简单工厂自身可归属客户端,它却持有全部客户。其实这段我不知道怎么表达才好!不知道如何描述这种不完美!
- 采用上述工厂方法后,创建函数从客户端移动到了具体插件自身,在宏的配合下,至少让我少些不少new,在有新插件加入时,对于客户端的修改也能少些几行代码。但至此版本依然不能逃脱"在客户端中有产品的痕迹",然后继续不屈不挠了2天。并且坚信,若使用反射机制,上述代码就会变得优雅许多。
[ps] 关于产品名称和C++宏
//创建过程
#define IMPLIMENT_DYNAMIC_CREATE(class_name) \
static CBaseClass* CreateClass##class_name() \
{ return new class_name; } \
//注册创建过程
#define REGISTER_CLASS(class_name) \
RegisterFactory(class_name::CreateClass##class_name, #class_name);\
关于C++宏的使用和伟大,仅做如此的补充,它确实为实现C++反射提供了巨大支持。注册接口上只保留一个类名,使用宏定义将class_name类类型名称转换为函数名称和字符串。另外,如果不追求java反射的用类字符串名创建的表象,这里的注册参数完全的可以使用一个单独的容易记录和查询的别名字符串或者ID数值。
这里还欠缺一个把类类型名称转为字符串的测试!如果#class_name可行,那还要Qt的metaObject.classname干甚—
[ps] 工厂方法的不正规形式
//静态成员函数 //cpp
static AbstrctPlugin* Plugin_A::CreateClassPluginA()
{
return new Plugin_A();
}
//非成员函数 //cpp
AbstrctPlugin* CreateClassPluginA()
{
AbstrctPlugin *t = new Plugin_A();
return (t == NULL)? NULL:t;
}
相比正规的工厂方法模式中强迫具体产品附件创建一个具体工厂类,上述两种方法略显温和,但心里总有一丝纠结尚不清晰。相对而言,具体工厂类允许你在其中附加对象创建的其他处理,如更详细的创建条件、实例数目控制、异常处理等,确实略胜一筹,但在要求不高时,个人感觉基本一致。
更自动化的工厂-1
依然在模块间高内聚低耦合的路上小奔,粗俗的理解了一下反射是如何实现模块的高内聚的,即使用真正反射技术后,执行具体对象创建的客户端,不应该不需要包含具体对象的头文件,而真正仅仅只是用了一个名字。如何让工厂方法更加的高仿Java反射样式?如何让工厂方法模式看上去更加温和些?如何工厂方法更加的自动化一些?
个人还是比较认同下边这种工厂方法的升华方式,在GOF工厂方法[9实现(4)-P74]中其实有提到:#GOF 工厂方法的一个潜在的问题就是它们可能仅为了创建适当的Product对象而迫使你创建Creator子类。C++中的一个解决办法是提供Creator的一个模板子类,它使用Product类作为模板参数 #GOF。 相关概要代码:
//使用模板避免创建子类
class Creator{
public:
virtual Product* CreateProduct() = 0;
}
//模板类
template <class TheProduct>
class StandardCreator : public Creator{
public:
virtual Product* CreateProduct();
}
template <class TheProduct>
Product* StandardCreator<TheProduct>::CreateProduct(){
return new TheProduct;
}
//客户端
StandardCreator<MyProduct> myCreator; //完成了一个具体工厂的定义
StandardCreator 与 StandardCreator 是2个类? Yes,模板类和类模板不太清楚的可去这略做参考,总的来说,类模板,名词重点在于模板,含义为生产类的模子,其实本质上类模板是一种依托于编译器的代码生成工具,在最终的代码中不会有模板本身的存在,只有模板具现出来的代码。模板类的重点是类,表示的是由一个模板生成而来的类,如QVector/QHash<int,int>都是模板类。
下边方法虽然没有使用模板而是宏操作,但是原理和效果上是一样样的。本质上REGISTER_CLASS这个类是工厂方法模式的具体工厂类(模板), 但这里没有定义抽象工厂类,因为用不到抽象工厂接口,因为"要用反射屏蔽在客户端中对不同具体工厂的判断(见基本目标-大话模式)"。
/** @brief MACRO features
* Here 每创建一个具体产品 都附加产生一个CAutoRegister对象、ProductRegister对象
* Here 借助了静态CAutoRegister变量的初始化时机在main入口前 完成函数指针的注册过程
* Here Macro invoked, class_name##Register表新类 与 template<typename T>异曲同工
* 本质上这个类是工厂方法模式的具体工厂类(模板) 但这里没有抽象工厂 因为用不到抽象工厂接口
**/
#define REGISTER_CLASS(class_name) \
class class_name##Creator { \
public: \
static void* CreateProduct() { \
return new class_name; \
} \
private: \
static const CAutoRegister ms_reg; \
};\
const CAutoRegister class_name##Creator::ms_reg(#class_name,class_name##Creator::CreateProduct);
/** @brief
* 该类相当于是对第三版本如下注册代码的包装 //具体产品类cpp中执行
* static int typeID = CMgrCreateFucs::registerCreateFunc(class_name, func);
**/
class CAutoRegister
{
public:
CAutoRegister(const string& class_name, register_func func)
{
CMgrCreateFucs::registerCreateFunc(class_name, func);
}
};
//创建函数的管理
class CMgrCreateFucs
{
public:
static void* NewInstance(const string& class_name);
static void RegisterClass(const string& class_name, register_func func);
private:
static map<string, register_func> ms_CreaterFuncs;
}
//与客户端关联的工厂(简单工厂或工厂方法使用反射后的最终样子)
AbstrctPlugin* createBank(const string& c_pluginName) {
return (AbstrctPlugin*)CMgrCreateFucs::NewInstance(c_pluginName);
}
关于C++反射实现的主体思想的总结,文章-K中的相关内容对我帮助很大,从代码命名上(如创建函数管理类取名Class-在向Java风靠拢),我想这位博主对Java可能比较熟悉。创建函数的注册时机,虽然不起眼但却是关键的一步,相比前边的版本,正是这步实现了产品与客户端的解耦!
更自动化的工厂-2
点题,该模式的关键在于C++模板技术的巧妙应用。This Code 据说是翻来的,不讨论他的出处,直接上代码做注解。对其中函数指针的使用、对QObject的依赖性作了修改,具体见代码注释。constructorHelper 是模板函数,T不同函数指针自然不同,不必多虑。
/** @brief 工厂类模板的实现
原实现中,该模板对QObject产生依赖,是因为其中用的staticMetaObject.className()是QObject的函数
但假若我们并不追求一定用类名称来做键,而是允许直接注册任何的别名或ID,则这个依赖不攻自破。
**/
class ObjectFactory
{
public:
template<typename T>
static void registerClass(const QString &c_strAlias) {
//可不使用T::staticMetaObject.className()元对象函数
constructors().insert(c_strAlias, constructorHelper<T>);
}
static AbstractPlugin* createObject(const QByteArray& className, QObject* parent = NULL) {
//获取创建函数的函数指针
Constructor constructor = constructors().value(className);
if (constructor == NULL)
return NULL;
return (constructor)(parent);
}
private:
typedef AbstractPlugin* (*Constructor)(QObject* parent);
//创建函数
template<typename T> static AbstractPlugin* constructorHelper(QObject* parent) {
return new T(parent);
}
//定义一个单例容器 //名称+创建函数指针
static QHash<QByteArray, Constructor>& constructors() {
static QHash<QByteArray, Constructor> instance;
return instance;
}
};
//具体产品 //cpp
ObjectFactory::registerClass<ConcreteProduct_A>("PluginA_ALIAS");
//客户端的调用
AbstractPlugin* pConcrete = ObjectFactory::createObject( "PluginA_ALIAS");
该方案相比于第三版,主要优点在于减少了具体类中关于创建函数的实现,这归功于模板的魅力。
回归本意
我的本意,是不在任何的客户端暴露具体对象(除了具体类自己的cpp以外的其他任何地方没有其头文件引用),其实满足下边几个条件,不借用Qt元对象系统,也不借助于C++宏操作,就可以:
1、对象类从同一个超类派生
2、建立各自的对象创建函数(类函数或模板…)
3、借助类似全局变量的初始化时机完成委托过程
但是上述思路的实现过程却大不同,可复杂可简洁,可像或不像反射的样子。
C++反射的基本缺陷:
可能我要的"反射效果",并不是该机制的精髓用途所在?
在一个真正支持反射的框架中,带二个条件由框架来做?反正在我使用了Qt元对象机制的反射Demo中,这个条件还是具体类自己来做的!Javaz中呢?初步向Java的同事请教后,在Java语言中,声明的所有类都可以直接用反射,并不需要这样的注册过程。
上述基于工厂的C++反射并不是真正的反射,故也没有Java反射那样的缺点,如效率问题。
构造函数的参数:
C++反射和Java反射是如何传递构造参数的?
使用普通C++反射时,能否为创建函数增加形参,用以带参数的构造函数?待整理。
配置文件的使用:
当倒回来重新审视我的业务场景时,通过配置文件控制插件是否加载在场景内并不那么的迫切。因为他们不是连续加载的,而是需要哪个加载哪个,这是倒是可以起到一种"类似插件购买权限制的效果",当配置中没有时,提示没有相应插件。
序列化与反射
MFC序列化、Qt序列化、及其各自的反序列化,是否会与反射机制有关系呢,尤其是Qt的流操作–
Qt反射技术
Qt反射技术理论支持,案例支持。看到这两篇文章,感觉又打开了一片新天地。
关于Qt反射技术的讲解,请参见另一篇博客C++反射(2)- Qt元对象系统与反射机制,这里仅列举4.8中借助QMetaType的一种实现方案–
讲解说明下,Qt元对象系统与反射机制的关系,总得来说,Java、C#的反射系统,都有一套类似元对象系统的东西存在。