反射機(jī)制在java中是一個(gè)非常重要的特性,比如在spring框架中,就使用了xml + 反射來(lái)完成類的動(dòng)態(tài)擴(kuò)展。
簡(jiǎn)單來(lái)說(shuō),反射就是根據(jù)一個(gè)字符串查找到一個(gè)類,查找到一個(gè)函數(shù),并能生成這個(gè)類,或者函數(shù)的實(shí)例。換句話說(shuō),就是在運(yùn)行期通過(guò)字符串到內(nèi)存單元的反向映射。
在動(dòng)態(tài)語(yǔ)言中,反射是非常容易實(shí)現(xiàn)的,在java中,由于有字節(jié)碼作為中間層的存在,所以實(shí)現(xiàn)也并不復(fù)雜。但是對(duì)于c++來(lái)說(shuō),靜態(tài)編譯的程序在運(yùn)行期能打交道的,只是一些內(nèi)存地址,沒(méi)有任何字符串相關(guān)的東西,也就是說(shuō),你寫程序時(shí)的那些變量名,真正編譯結(jié)束后,只是一些內(nèi)存地址。
當(dāng)然了,萬(wàn)事沒(méi)有絕對(duì)。 要想在c++中實(shí)現(xiàn)類似java的反射功能,也是可以做到的,比如——增加中間層。
unix設(shè)計(jì)藝術(shù)里曾說(shuō)過(guò):所有的問(wèn)題都可以通過(guò)增加中間層的方式來(lái)解決。
增加中間層也分兩種,下面先說(shuō)第一種:
1 編譯期增加中間層
我們知道,程序在編譯期,是有一個(gè)全局的符號(hào)對(duì)照表的,這個(gè)里面保存了字符串(變量或者類名)到內(nèi)存地址的映射,但是一旦編譯完成,在鏈接時(shí),這些“多余”的信息都將不存在。因此只需要找到一個(gè)方式,能把這些信息保存下來(lái),就可以了。順著這個(gè)思路想下去,就會(huì)發(fā)現(xiàn),我們只要把靜態(tài)鏈接改為動(dòng)態(tài)鏈接,就可以將需要的符號(hào)信息保存下來(lái)。
具體來(lái)說(shuō):
(1) 將所有需要?jiǎng)討B(tài)創(chuàng)建的類放置于一個(gè)或多個(gè)獨(dú)立的文件中;
(2) 將這些文件編譯為一個(gè).so文件;
(3) 在主程序中通過(guò)dlopen打開(kāi)so文件;
(4) 調(diào)用dlsym(*pvSo, "create")獲得需要的函數(shù);
在dlsym中,完成了從字符串到函數(shù)的映射。
這種方式其實(shí)很不錯(cuò)的,但是,對(duì)于c++來(lái)說(shuō),函數(shù)的命名方式和c完全不同,所以需要通過(guò)各種extern來(lái)修飾函數(shù)。而且,由于使用了動(dòng)態(tài)編譯,在so中就不能再使用一些模板的特性了。當(dāng)然了,還有一些莫名其妙的很奇葩的坑,就不一一細(xì)說(shuō)了。
簡(jiǎn)單的示例代碼如下:
//test.so
class Test
{
public:
void fun() {}
};
extern "C" Test* create()
{
return new CheckBankAtm;
}
//main
int main()
{
void* so = dlopen("test.so", RTLD_LAZY);
Test* (*fun)();
fun = (Test* (*) ()) dlsym(so, "create");
}
2 運(yùn)行期增加中間層
由于動(dòng)態(tài)編譯存在各種各樣的問(wèn)題,所以我們還是更傾向于選擇靜態(tài)編譯。也就是說(shuō)在運(yùn)行期增加一個(gè)字符串到函數(shù)的中間層。我們需要準(zhǔn)備一個(gè)全局的map,以字符串為key,函數(shù)指針作為value。同時(shí)提供一個(gè)注冊(cè)機(jī)制,把每個(gè)需要的類注冊(cè)到這個(gè)全局map中。這里還剩下最后一個(gè)問(wèn)題,也是最重要的問(wèn)題:誰(shuí)來(lái)完成這個(gè)注冊(cè)的過(guò)程?
如果是調(diào)用者來(lái)注冊(cè),那么首先,我們需要在調(diào)用的文件中include用到的所有類的頭文件,之后,我們需要一個(gè)一個(gè)把類注冊(cè)進(jìn)來(lái),如果需要注冊(cè)的類有上千個(gè),這個(gè)過(guò)程將會(huì)非常的繁雜。
如果是類的作者自己完成注冊(cè),就涉及到另一個(gè)問(wèn)題,如何注冊(cè)?因?yàn)樽?cè)的這個(gè)過(guò)程,本身需要在類外面,而且還必須是一段可執(zhí)行代碼。這樣就又繞回了上面那步,誰(shuí)來(lái)調(diào)用注冊(cè)代碼?
可以通過(guò)一個(gè)比較取巧的辦法:類的靜態(tài)對(duì)象。
我們知道,類的靜態(tài)對(duì)象是全局的,而所有的全局變量,編譯器保證了一定會(huì)在main執(zhí)行前被初始化,換句話說(shuō),我們只要把注冊(cè)代碼放置于類的靜態(tài)變量的初始化過(guò)程中,就可以了。
示例代碼如下:
#include <iostream>
#include <string>
#include <map>
using namespace std;
#define BASE_CLASS Test
#define GLOBAL_FUN_MAP FunMap<BASE_CLASS>::get_fun_map()
#define DEFINE_CLASS(class_name, fun_name) \
class_name(std::string) \
{\
GLOBAL_FUN_MAP.regist(#fun_name, class_name::fun_name);\
}\
class_name(){}\
static class_name class_name##_;\
static BASE_CLASS* fun_name()\
{\
return new class_name;\
}
#define REGIST_CLASS(class_name) \
class_name class_name::class_name##_(#class_name);
template <class T>
class FunMap
{
typedef T* (*FUN)(void);
map<std::string, FUN> fun_map_;
public:
void regist(string fun_name, FUN fun)
{
fun_map_[fun_name] = fun;
}
T* get(const string fun_name)
{
if (fun_map_.end() != fun_map_.find(fun_name))
{
return fun_map_[fun_name]();
}
else
{
return NULL;
}
}
static FunMap<T>& get_fun_map()
{
static FunMap<T> fun_map;
return fun_map;
}
};
class Test
{};
class Test1 : public Test
{
public:
DEFINE_CLASS(Test1, test1)
};
class Test2 : public Test
{
public:
DEFINE_CLASS(Test2, test2)
};
REGIST_CLASS(Test1)
REGIST_CLASS(Test2)
int main()
{
GLOBAL_FUN_MAP.get("test1");
GLOBAL_FUN_MAP.get("test2");
GLOBAL_FUN_MAP.get("11111111");
}
利用了多態(tài)和模板增加了程序的靈活性,同時(shí)使用了宏來(lái)簡(jiǎn)化代碼。
使用時(shí),需要在類中增加如下代碼:
DEFINE_CLASS(Test1, test1) //第一個(gè)參數(shù)為類名,第二個(gè)參數(shù)為字符串名
同時(shí),需要在類的實(shí)現(xiàn)中增加:
REGIST_CLASS(Test1) //參數(shù)為類名