c++實(shí)現(xiàn)類java反射:從類名字符串創(chuàng)建對(duì)象

前言

最近在項(xiàng)目中,需要用到從類名來(lái)創(chuàng)建C++類對(duì)象,類似于Java中的反射。C++沒(méi)有反射的概念,所以是沒(méi)辦法和Java一樣通過(guò)類名來(lái)創(chuàng)建對(duì)象。

思考了幾種方式之后,我得到了一種性能和代碼上都比較不錯(cuò)的方式。如果急著尋求方案,可以直接滑到總結(jié)處。

核心思路

眾多方式,其實(shí)本質(zhì)的核心思路是一樣的:使用一個(gè)Map來(lái)保存字符串和創(chuàng)建對(duì)象的函數(shù)

寫個(gè)偽代碼大概就是這樣

std::map<std::string,std::function<...>> registerMap;

void registerObject(std::function<...>) {
    ...;
    registerMap.insert(...);
}

Object createObject(const std::string& name) {
    ...;
    return registerMap[name]();
}

那么現(xiàn)在就有兩個(gè)重要問(wèn)題需要解決:

  1. map中的function類型如何確定
  2. 如何優(yōu)雅地對(duì)類型進(jìn)行注冊(cè)

注冊(cè)function類型

注冊(cè)的function,需要?jiǎng)?chuàng)建并返回我們需要的對(duì)象類型,例如我們需要?jiǎng)?chuàng)建一個(gè)Student對(duì)象:

std::unique_ptr<Student> create(const std::string name) {
    return std::unique_ptr<Student>(new Student(name));
}

但是我們會(huì)發(fā)現(xiàn),我們必須指定function的返回值以及構(gòu)造參數(shù)模板,而每個(gè)對(duì)象的所對(duì)應(yīng)的返回值和構(gòu)造函數(shù)參數(shù)都不同,因此需要進(jìn)行統(tǒng)一。

  • 對(duì)于構(gòu)造函數(shù)參數(shù),這里全部設(shè)計(jì)為無(wú)參,并將初始化邏輯遷移到init方法中,這樣可以簡(jiǎn)化構(gòu)建的邏輯。
  • 返回值類型,可以讓所有需要反射創(chuàng)建對(duì)象的類繼承同個(gè)基類,這樣可以統(tǒng)一函數(shù)的返回值類型。隨后再通過(guò)dynamic_cast進(jìn)行指針轉(zhuǎn)型。

這里我們?cè)O(shè)計(jì)頂層的基類是Object,注意其析構(gòu)函數(shù)必須為虛函數(shù):

class Object {

public:
    virtual ~Object() = default;
};

我們的注冊(cè)函數(shù)類型可以設(shè)計(jì)為:std::function<std::unique_ptr<Object>()>。這里采用智能指針的原因是告訴調(diào)用方需要自己負(fù)責(zé)對(duì)象內(nèi)存的釋放,避免造成內(nèi)存泄露。

設(shè)計(jì)后的函數(shù)接口是:

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]();
    // 從基類動(dòng)態(tài)轉(zhuǎn)換為外部需要的類型
    return std::unique_ptr<T>(dynamic_cast<T*>(ptr.release()));
}

外部使用的時(shí)候如下代碼:

class Student : public Object {}

// 注冊(cè)
registerObject("Student",[]()->std::unique_ptr<Object> {
    auto *student = new Student();
    return std::unique_ptr<Object>(dynamic_cast<Object*>(student));
});

//創(chuàng)建
auto student = createObject<Student>("Student");

我們會(huì)發(fā)現(xiàn),每次注冊(cè)的時(shí)候都需要寫一個(gè)lambda,不同的類型的結(jié)構(gòu)基本相同。這里我們可以利用模板編程來(lái)簡(jiǎn)化一下:

template <typename T>
std::unique_ptr<Object> make_unique() {
    auto* ptr = new T;
    return std::unique_ptr<Object>(dynamic_cast<Object*>(ptr));
}

這樣,注冊(cè)的時(shí)候就簡(jiǎn)單了:

// 注冊(cè)
registerObject("Student",make_unique<Student>);

注冊(cè)時(shí)機(jī)

有了上面的邏輯,其實(shí)已經(jīng)可以運(yùn)行起來(lái)了。舉個(gè)例子:

class Student : public Object{
public:
    void func(){} 
};

int main() {
    // 注冊(cè)
    registerObject("Student",make_unique<Student>);
    // 通過(guò)名稱創(chuàng)建對(duì)象
    auto ptr = createObject<Student>("Student");
    ptr->func();
}

這種寫法可行,但是具體到項(xiàng)目中,存在以下問(wèn)題:

  • 我們需要在程序運(yùn)行前,需要在一個(gè)全局初始化的地方,手動(dòng)對(duì)所有需要反射創(chuàng)建的類進(jìn)行注冊(cè)。
  • 每創(chuàng)建一個(gè)新的類,那么就需要在這個(gè)初始化的地方添加一行注冊(cè)代碼。
  • 全局初始化的地方需要include所有需要反射創(chuàng)建的類的頭文件,造成文件大小急劇膨脹。當(dāng)然這個(gè)問(wèn)題可以用宏來(lái)解決,但是代碼會(huì)更加復(fù)雜。

我們的注冊(cè)邏輯需要滿足以下特性:

  • 每個(gè)類自己負(fù)責(zé)注冊(cè),這樣我們可以更加靈活地增刪類而不需要去修改全局注冊(cè)點(diǎn)
  • 保證全局唯一性
  • 不要帶來(lái)太多的性能損耗

這里我采用的注冊(cè)方法是:利用靜態(tài)屬性的唯一性以及提前性初始化,在其構(gòu)造函數(shù)中對(duì)類型進(jìn)行注冊(cè) 。

舉個(gè)例子,先看代碼:

class Student : public Object{
    static struct RegisterTask {
        RegisterTask() {
            // 對(duì)Student進(jìn)行注冊(cè)
        }
    } task;
};

上面代碼中,我在類Student中添加了一個(gè)RegisterTask內(nèi)部結(jié)構(gòu)體,并聲明了一個(gè)靜態(tài)變量。task靜態(tài)變量會(huì)在全局代碼執(zhí)行前被初始化,其構(gòu)造函數(shù)會(huì)被調(diào)用,我們可以在構(gòu)造函數(shù)中對(duì)Student進(jìn)行注冊(cè)。

這里有兩個(gè)注意點(diǎn):

  • 靜態(tài)屬性需要在類外進(jìn)行初始化
  • 類的靜態(tài)屬性如果沒(méi)有被使用到,他可能會(huì)延遲初始化,這不符合我們的需求

結(jié)合上面兩點(diǎn),我們把這一塊的代碼,遷移到cpp文件中,把類的靜態(tài)屬性修改為全局屬性,保證其初始化同時(shí)不需要在類再寫一句代碼進(jìn)行初始化??聪麓a:

Student.h
class Student : public Object {
};

Student.cpp
static struct RegisterTask {
    RegisterTask() {
        registerObject("Student",make_unique<Student>);
    }
} task;

這里我們可以再優(yōu)化下,這樣每個(gè)類我們都需要編寫相同的代碼,但是內(nèi)容卻只是相差一個(gè)類型。我們可以采用宏來(lái)解決這個(gè)問(wèn)題,如下:

#ifndef REGISTER_CLASS
#define REGISTER_CLASS(Type)\
static struct _ObjectRegisterTask##Type { \
    _ObjectRegisterTask##Type() { \
        registerObject(#Type,make_unique<Type>); \
    }; \
} _task##Type; // NOLINT
#endif

這里有個(gè)需要注意的點(diǎn):

  • 由于我們把結(jié)構(gòu)體和變量設(shè)置為全局屬性,那么就需要注意重名問(wèn)題,所以這里我們把類名和變量名都做了去重名處理

這樣,我們只需要讓需要被注冊(cè)的類include此頭文件,并在cpp源文件中,聲明這個(gè)宏,如下:

Student.h
class Student : public Object {
};

Student.cpp
REGISTER_CLASS(Student)

就可以調(diào)用我們前面的全局方法createObject來(lái)創(chuàng)建對(duì)象了。

到這里其實(shí)就差不多了,但我們還可以再優(yōu)化一下性能。會(huì)發(fā)現(xiàn)按照上面的方法,我們每個(gè)需要?jiǎng)討B(tài)創(chuàng)建的類都會(huì)創(chuàng)建一個(gè)結(jié)構(gòu)體,這在包大小有要求的場(chǎng)景或者對(duì)內(nèi)存極為苛刻的嵌入式中還是有一些影響。

我們需要多個(gè)不同的結(jié)構(gòu)體的原因是我們需要在不同的的結(jié)構(gòu)體的構(gòu)造方法中,對(duì)不同的類型分別注冊(cè)。優(yōu)化的要點(diǎn)是,我們可以把這個(gè)注冊(cè)遷移到外部。還是以類型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>));

可以看到,我們讓結(jié)構(gòu)的構(gòu)造函數(shù)要求一個(gè)int參數(shù),然后我們調(diào)用另一個(gè)函數(shù)來(lái)獲取int值,那么我們就可以在這個(gè)函數(shù)中去做注冊(cè)相關(guān)的事情了,這樣就減少了類型的創(chuàng)建。

最后還是老方法,利用宏來(lái)簡(jiǎn)化類的注冊(cè)邏輯:

#ifndef REGISTER_CLASS
#define REGISTER_CLASS(Type)\
static RegisterTask  task##Type(RegisterTask::registerfun(#Type,make_unique<Type>));
#endif

總結(jié)

最后對(duì)我們上面的方法做個(gè)回顧:

  1. 首先我們需要編寫一個(gè)頂層基類,注意需要把析構(gòu)函數(shù)標(biāo)記為virtual,如果需要可以添加其他的虛函數(shù)
class Object {

public:
    virtual ~Object() = default;
};
  1. 編寫一個(gè)注冊(cè)頭文件,利用模板設(shè)計(jì)注冊(cè)接口,將注冊(cè)信息保存在全局map數(shù)據(jù)結(jié)構(gòu)中
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]();
    // 從基類動(dòng)態(tài)轉(zhuǎn)換為外部需要的類型
    return std::unique_ptr<T>(dynamic_cast<T*>(ptr.release()));
}
  1. 編寫創(chuàng)建類型的函數(shù),簡(jiǎn)化類型注冊(cè)的代碼邏輯:
template <typename T>
std::unique_ptr<Object> make_unique() {
    auto* ptr = new T;
    return std::unique_ptr<Object>(dynamic_cast<Object*>(ptr));
}
  1. 編寫注冊(cè)相關(guān)的結(jié)構(gòu)以及宏:
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. 編寫我們需要反射創(chuàng)建的類,繼承自基類Object,并使用宏來(lái)進(jìn)行注冊(cè),注意這里宏使用需要放在源文件中。然后調(diào)用createObject方法就可以構(gòu)建對(duì)象了。
Student.h
class Student : public Object {
};

Student.cpp
REGISTER_CLASS(Student)

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

以上就是這套方法的完整使用流程。對(duì)于新增加的類,只需要繼承基類obejct,并使用宏即可,還是比較方便和靈活的,不需要每次增刪一個(gè)類都得修改全局注冊(cè)的地方。

性能上的損耗就是每個(gè)類需要實(shí)例化注冊(cè)函數(shù)模板,但這一塊的代碼非常少,影響的范圍也是比較小的。
當(dāng)然具體到項(xiàng)目中可能需要再做一些優(yōu)化,把接口設(shè)計(jì)得更加通用一點(diǎn),根據(jù)具體的項(xiàng)目情況做一些case的保護(hù)等等,但核心的思路不變。

如果文章內(nèi)容對(duì)你有幫助,還希望可以給作者留個(gè)贊鼓勵(lì)一下~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容