前言
最近在项目中,需要用到从类名来创建C++类对象,类似于Java中的反射。C++没有反射的概念,所以是没办法和Java一样通过类名来创建对象。
思考了几种方式之后,我得到了一种性能和代码上都比较不错的方式。如果急着寻求方案,可以直接滑到总结处。
核心思路
众多方式,其实本质的核心思路是一样的:使用一个Map来保存字符串和创建对象的函数 。
写个伪代码大概就是这样
std::map<std::string,std::function<...>> registerMap;
void registerObject(std::function<...>) {
...;
registerMap.insert(...);
}
Object createObject(const std::string& name) {
...;
return registerMap[name]();
}
那么现在就有两个重要问题需要解决:
- map中的function类型如何确定
- 如何优雅地对类型进行注册
注册function类型
注册的function,需要创建并返回我们需要的对象类型,例如我们需要创建一个Student对象:
std::unique_ptr<Student> create(const std::string name) {
return std::unique_ptr<Student>(new Student(name));
}
但是我们会发现,我们必须指定function的返回值以及构造参数模板,而每个对象的所对应的返回值和构造函数参数都不同,因此需要进行统一。
- 对于构造函数参数,这里全部设计为无参,并将初始化逻辑迁移到init方法中,这样可以简化构建的逻辑。
- 返回值类型,可以让所有需要反射创建对象的类继承同个基类,这样可以统一函数的返回值类型。随后再通过dynamic_cast进行指针转型。
这里我们设计顶层的基类是Object,注意其析构函数必须为虚函数:
class Object {
public:
virtual ~Object() = default;
};
我们的注册函数类型可以设计为:std::function<std::unique_ptr<Object>()>
。这里采用智能指针的原因是告诉调用方需要自己负责对象内存的释放,避免造成内存泄露。
设计后的函数接口是:
using RegisterFunc = std::function<std::unique_ptr<Object>()>
std::map<std::string ,RegisterFunc> objectMap;
void registerObject(const std::string& name,RegisterFunc func) {
objectMap.insert({name,func});
}
template <typename T>
std::unique_ptr<T> createObject(const std::string& name) {
if (objectMap.find(name) == objectMap.end()) {
return nullptr;
}
auto ptr = objectMap[name]();
// 从基类动态转换为外部需要的类型
return std::unique_ptr<T>(dynamic_cast<T*>(ptr.release()));
}
外部使用的时候如下代码:
class Student : public Object {}
// 注册
registerObject("Student",[]()->std::unique_ptr<Object> {
auto *student = new Student();
return std::unique_ptr<Object>(dynamic_cast<Object*>(student));
});
//创建
auto student = createObject<Student>("Student");
我们会发现,每次注册的时候都需要写一个lambda,不同的类型的结构基本相同。这里我们可以利用模板编程来简化一下:
template <typename T>
std::unique_ptr<Object> make_unique() {
auto* ptr = new T;
return std::unique_ptr<Object>(dynamic_cast<Object*>(ptr));
}
这样,注册的时候就简单了:
// 注册
registerObject("Student",make_unique<Student>);
注册时机
有了上面的逻辑,其实已经可以运行起来了。举个例子:
class Student : public Object{
public:
void func(){}
};
int main() {
// 注册
registerObject("Student",make_unique<Student>);
// 通过名称创建对象
auto ptr = createObject<Student>("Student");
ptr->func();
}
这种写法可行,但是具体到项目中,存在以下问题:
- 我们需要在程序运行前,需要在一个全局初始化的地方,手动对所有需要反射创建的类进行注册。
- 每创建一个新的类,那么就需要在这个初始化的地方添加一行注册代码。
- 全局初始化的地方需要include所有需要反射创建的类的头文件,造成文件大小急剧膨胀。当然这个问题可以用宏来解决,但是代码会更加复杂。
我们的注册逻辑需要满足以下特性:
- 每个类自己负责注册,这样我们可以更加灵活地增删类而不需要去修改全局注册点
- 保证全局唯一性
- 不要带来太多的性能损耗
这里我采用的注册方法是:利用静态属性的唯一性以及提前性初始化,在其构造函数中对类型进行注册 。
举个例子,先看代码:
class Student : public Object{
static struct RegisterTask {
RegisterTask() {
// 对Student进行注册
}
} task;
};
上面代码中,我在类Student中添加了一个RegisterTask内部结构体,并声明了一个静态变量。task静态变量会在全局代码执行前被初始化,其构造函数会被调用,我们可以在构造函数中对Student进行注册。
这里有两个注意点:
- 静态属性需要在类外进行初始化
- 类的静态属性如果没有被使用到,他可能会延迟初始化,这不符合我们的需求
结合上面两点,我们把这一块的代码,迁移到cpp文件中,把类的静态属性修改为全局属性,保证其初始化同时不需要在类再写一句代码进行初始化。看下代码:
Student.h
class Student : public Object {
};
Student.cpp
static struct RegisterTask {
RegisterTask() {
registerObject("Student",make_unique<Student>);
}
} task;
这里我们可以再优化下,这样每个类我们都需要编写相同的代码,但是内容却只是相差一个类型。我们可以采用宏来解决这个问题,如下:
#ifndef REGISTER_CLASS
#define REGISTER_CLASS(Type)\
static struct _ObjectRegisterTask##Type { \
_ObjectRegisterTask##Type() { \
registerObject(#Type,make_unique<Type>); \
}; \
} _task##Type; // NOLINT
#endif
这里有个需要注意的点:
- 由于我们把结构体和变量设置为全局属性,那么就需要注意重名问题,所以这里我们把类名和变量名都做了去重名处理
这样,我们只需要让需要被注册的类include此头文件,并在cpp源文件中,声明这个宏,如下:
Student.h
class Student : public Object {
};
Student.cpp
REGISTER_CLASS(Student)
就可以调用我们前面的全局方法createObject
来创建对象了。
到这里其实就差不多了,但我们还可以再优化一下性能。会发现按照上面的方法,我们每个需要动态创建的类都会创建一个结构体,这在包大小有要求的场景或者对内存极为苛刻的嵌入式中还是有一些影响。
我们需要多个不同的结构体的原因是我们需要在不同的的结构体的构造方法中,对不同的类型分别注册。优化的要点是,我们可以把这个注册迁移到外部。还是以类型Student为例子,如下代码:
struct RegisterTask {
RegisterTask_(int) {
// do nothing
}
static int registerfun(const std::string& name,
std::function<std::unique_ptr<Object>()> func) {
registerObject(name,func);
return 0;
}
};
static RegisterTask task(RegisterTask::registerfun("Student",make_unique<Student>));
可以看到,我们让结构的构造函数要求一个int参数,然后我们调用另一个函数来获取int值,那么我们就可以在这个函数中去做注册相关的事情了,这样就减少了类型的创建。
最后还是老方法,利用宏来简化类的注册逻辑:
#ifndef REGISTER_CLASS
#define REGISTER_CLASS(Type)\
static RegisterTask task##Type(RegisterTask::registerfun(#Type,make_unique<Type>));
#endif
总结
最后对我们上面的方法做个回顾:
- 首先我们需要编写一个顶层基类,注意需要把析构函数标记为virtual,如果需要可以添加其他的虚函数
class Object {
public:
virtual ~Object() = default;
};
- 编写一个注册头文件,利用模板设计注册接口,将注册信息保存在全局map数据结构中
using RegisterFunc = std::function<std::unique_ptr<Object>()>
std::map<std::string ,RegisterFunc> objectMap;
void registerObject(const std::string& name,RegisterFunc func) {
objectMap.insert({name,func});
}
template <typename T>
std::unique_ptr<T> createObject(const std::string& name) {
if (objectMap.find(name) == objectMap.end()) {
return nullptr;
}
auto ptr = objectMap[name]();
// 从基类动态转换为外部需要的类型
return std::unique_ptr<T>(dynamic_cast<T*>(ptr.release()));
}
- 编写创建类型的函数,简化类型注册的代码逻辑:
template <typename T>
std::unique_ptr<Object> make_unique() {
auto* ptr = new T;
return std::unique_ptr<Object>(dynamic_cast<Object*>(ptr));
}
- 编写注册相关的结构以及宏:
struct RegisterTask {
RegisterTask_(int) {
// do nothing
}
static int registerfun(const std::string& name,
std::function<std::unique_ptr<Object>()> func) {
registerObject(name,func);
return 0;
}
};
static RegisterTask task(RegisterTask::registerfun("Student",make_unique<Student>));
#ifndef REGISTER_CLASS
#define REGISTER_CLASS(Type)\
static RegisterTask task##Type(RegisterTask::registerfun(#Type,make_unique<Type>));
#endif
- 编写我们需要反射创建的类,继承自基类Object,并使用宏来进行注册,注意这里宏使用需要放在源文件中。然后调用createObject方法就可以构建对象了。
Student.h
class Student : public Object {
};
Student.cpp
REGISTER_CLASS(Student)
auto studentPtr = createObject<Student>("student");
以上就是这套方法的完整使用流程。对于新增加的类,只需要继承基类obejct,并使用宏即可,还是比较方便和灵活的,不需要每次增删一个类都得修改全局注册的地方。
性能上的损耗就是每个类需要实例化注册函数模板,但这一块的代码非常少,影响的范围也是比较小的。
当然具体到项目中可能需要再做一些优化,把接口设计得更加通用一点,根据具体的项目情况做一些case的保护等等,但核心的思路不变。
如果文章内容对你有帮助,还希望可以给作者留个赞鼓励一下~