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閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容