问题的起因是,我在做一个demo,有一个对象基类,以及一堆派生出的子对象,比如球体、立方体之类的对象。还有一个对象管理类,用于存储场景中的所有对象。那么在初始化的时候,代码是这么写的:
class objectInfo
{
private:vector<object*> vecObjs;
public:void Init(){vector name ={"Sphere","Cube","Cube","Cone",};for (int i = 0; i < name.size(); i ){object* obj = nullptr;if (name[i] == "Sphere"){obj = new Sphere();}else if (name[i] == "Cube"){obj = new Cube();}else if (name[i] == "Cone"){obj = new Cone();}if (obj){vecObjs.push_back(obj);}// ....}}
};
虽然是不会有什么问题,但感觉这么写真的很蠢。所以开始研究自动做这件事的方法,也就是说,只要调一次Create()即可,不需要像上面这样写一长串的if / else,否则代码会比较难维护。
现在,问题可以简化为,给定一个字符串,自动构造出对应的对象。
C 目前已经支持了RTTI技术,在已有一个类的实例的时候,可以获取类的名字(字符串形式的)。但这点似乎并不能派上用场,因为我们需要在得到实例之前去做从字符串到类的解析。据我所知,大部分C 框架都会定义很多宏,来完成代码生成的过程,所以在使用这些框架的时候,每次定义一个类,都要加一堆奇奇怪怪的大写字符。我用过的包含MFC,Qt和Unreal,但具体实现细节,我还没有深究过。
依照代码生成的思路,我以一个C 弱鸡的角度,思考如果我自己去做这么一个自动生成器,应该怎么实现。(也就是我的做法大概率不是非常好的,如果希望实际使用的话,最好借鉴一些成熟的解决方案)
首先,要给每个类定义一个函数,比如对于类Sphere,应该有这么一个Create函数(当然,我们所有的讨论是基于类有继承关系,以下例子中类Sphere继承自object):
object* Create()
{return new Sphere();
}
这个函数也就是某个自动生成器最终调用的一个函数。首先它应该不能是类A成员函数,因为此时类A还没有实例化,是访问不到的。
为了根据字符串找到这个函数,我想到的是,可以存在一个字符串到函数的map里,它大概长这么个样子:
map<string, function<object*()>>
也就是说,我们在定义一个类后,还需要完成如下的操作:
(1) 生成一个对应的 Create()函数。
(2) 把这个函数加入到map里。
如果让宏来完成,那么Create函数的生成就是这样的:
#define CREATE(class_name) \object* Create() { return new class_name();};\
根据以上思路,又引入了一个单例的Helper类(可以理解为Factory)来封装一些东西,最终第一版是这样的:
#include
#include
#include
#include
#include
using namespace std;
class object;#define REGISTER(class_name) \Helper::Inst()->Push(#class_name,[]()->object* \{ return new class_name;});\class Helper
{
private:map<string, function<object*()>> mapStr2Func;static Helper* helper;Helper() {}
public:static Helper* Inst(){if (!helper){helper = new Helper();}return helper;}object* Createobject(string name){if (mapStr2Func.find(name) != mapStr2Func.end())return mapStr2Func[name]();return nullptr;}void Push(string name, function<object*()> func) { mapStr2Func[name] = func; }
};class object
{
public:virtual void Print() = 0;const char* GetClassName(){return typeid(*this).name();}
};class Sphere : public object
{void Print() override { cout << GetClassName() << endl; }
};class Cube : public object
{void Print() override { cout << GetClassName() << endl; }
};class Cone : public object
{void Print() override { cout << GetClassName() << endl; }
};class objectInfo
{
private:vector<object*> vecObjs;
public:void Init(){vector name ={"Sphere","Cube","Cube","Cone",};for (int i = 0; i < name.size(); i ){object* obj = Helper::Inst()->Createobject(name[i]);if (obj){vecObjs.push_back(obj);obj->Print();} }}
};Helper* Helper::helper = nullptr;
int main()
{REGISTER(Sphere)REGISTER(Cube)REGISTER(Cone)objectInfo obj;obj.Init();system("pause");
}
以上代码最终打印的结果为:
但这个代码有一处我非常不满意的地方,这个REGISTER宏只能放在函数体里,比如这里的main里,而我希望的是能够放在类声明的旁边。因为自动生成的代码包含了把函数放入map的过程,而这样的操作在全局空间中是不允许的,我们最多只能在全局空间写一些变量的声明加初始化,比如:
int x = 0;
但无法做:
int x ;
x = 0; // forbidden!
纠结这个宏所在位置的原因在于:类的声明和宏放在一起易于维护。
就我个人经验而言,如果项目中有这么一个类base,我想继承它做一个新的功能类,那么我一定会先参照它已有的另一个子类的代码,看它是如何写的,或者更直接的,我会把它复制过来,把不必要的东西删掉,留下必要的。如果宏和类的声明放在一起,那么这个东西我是不会落下的,照葫芦画瓢改一遍都不会出错。但作为新手而言,我肯定很难想到,我要到另外一个看起来毫不相干的类里,添加一句宏。
作为改进,为了让以上操作(map的insert操作),能顺利在全局空间执行,我又构造了一个类Generator,利用它的构造过程偷偷完成了这一过程,也就是我可以在全局空间写:
Generator* generator = new Generator();
然后把那一堆逻辑放在Generator的构造函数里。反正以上行为就是穿了个马甲。
在头文件中定义变量其实是不符合规范的,为了稳妥可以把这个宏挪到对应的实现文件里,但出于个人强迫症,我希望只在头文件声明就足以。我用的vs 2017竟然可以编译过,但不确定其它编译器是否可行。(难道这是msvc编译器的特性?)
最终的代码如下:
reflect.h
#pragma once
class object;#include
using namespace std;
#define REGISTER(class_name) \class class_name##Generator : public Generator{\public:class_name##Generator() { \Helper::Inst()->Push(#class_name, this);}\object* Create() {return new class_name();}};\class_name##Generator* class_name##Inst = new class_name##Generator();class Generator
{
public:virtual object* Create() = 0;
};class Helper
{
private:map mapStr2Generator;static Helper* helper;Helper() {}
public:static Helper* Inst(){if (!helper){helper = new Helper();}return helper;}object* Createobject(string name){if (mapStr2Generator.find(name) != mapStr2Generator.end())return mapStr2Generator[name]->Create();return nullptr;}void Push(string name, Generator* generator) { mapStr2Generator[name] = generator; }
};
test.h
#pragma once#include
#include
#include
#include
#include "reflect.h"class object
{
public:virtual void Print() = 0;const char* GetClassName(){return typeid(*this).name();}
};class Sphere : public object
{void Print() override { cout << GetClassName() << endl; }
};
REGISTER(Sphere)class Cube : public object
{void Print() override { cout << GetClassName() << endl; }
};
REGISTER(Cube)