c++实现类java反射:从类名字符串创建对象

前言

最近在项目中,需要用到从类名来创建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]();
}

那么现在就有两个重要问题需要解决:

  1. map中的function类型如何确定
  2. 如何优雅地对类型进行注册

注册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

总结

最后对我们上面的方法做个回顾:

  1. 首先我们需要编写一个顶层基类,注意需要把析构函数标记为virtual,如果需要可以添加其他的虚函数
class Object {

public:
    virtual ~Object() = default;
};
  1. 编写一个注册头文件,利用模板设计注册接口,将注册信息保存在全局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()));
}
  1. 编写创建类型的函数,简化类型注册的代码逻辑:
template <typename T>
std::unique_ptr<Object> make_unique() {
    auto* ptr = new T;
    return std::unique_ptr<Object>(dynamic_cast<Object*>(ptr));
}
  1. 编写注册相关的结构以及宏:
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
  1. 编写我们需要反射创建的类,继承自基类Object,并使用宏来进行注册,注意这里宏使用需要放在源文件中。然后调用createObject方法就可以构建对象了。
Student.h
class Student : public Object {
};

Student.cpp
REGISTER_CLASS(Student)

auto studentPtr = createObject<Student>("student");

以上就是这套方法的完整使用流程。对于新增加的类,只需要继承基类obejct,并使用宏即可,还是比较方便和灵活的,不需要每次增删一个类都得修改全局注册的地方。

性能上的损耗就是每个类需要实例化注册函数模板,但这一块的代码非常少,影响的范围也是比较小的。
当然具体到项目中可能需要再做一些优化,把接口设计得更加通用一点,根据具体的项目情况做一些case的保护等等,但核心的思路不变。

如果文章内容对你有帮助,还希望可以给作者留个赞鼓励一下~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容