前言
最近在項(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)題需要解決:
- map中的function類型如何確定
- 如何優(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è)回顧:
- 首先我們需要編寫一個(gè)頂層基類,注意需要把析構(gòu)函數(shù)標(biāo)記為virtual,如果需要可以添加其他的虛函數(shù)
class Object {
public:
virtual ~Object() = default;
};
- 編寫一個(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()));
}
- 編寫創(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));
}
- 編寫注冊(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
- 編寫我們需要反射創(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ì)一下~