前言
最近在項目中,需要用到從類名來創建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的保護等等,但核心的思路不變。
如果文章內容對你有幫助,還希望可以給作者留個贊鼓勵一下~