1. 程式人生 > >C++實現反射(即類似於.NET、java的反射機制)

C++實現反射(即類似於.NET、java的反射機制)

NET下的很多技術都是基於反射機制來實現的,反射讓.NET平臺下的語言變得得心應手。最簡單的,比如列舉型別,我們我可以很容易的獲得一個列舉變數的數值以及其名稱字串。

可是,在C++中,列舉變數本質上和一個整形變數沒有區別,我們很難獲取一個列舉變數的名稱字串。

其實在C++中,我們可以通過巨集來實現類似反射的機制。

接下來,我想總結一下如何在C++中實現一個類似於C#列舉型別的方法。

[cpp] view plaincopy

 

  1. __VA_ARGS__  
  2. 使用__VA_ARGS__,我們可以定義帶可變引數的巨集,舉個例子:  
  3.   
  4. #define MY_PRINTF(…) printf(__VA_ARGS__)  
  5.   
  6. 這樣我們寫  
  7.   
  8. MY_PRINTF("hello, %s”, "world");  
  9.   
  10. 就等價於  
  11.   
  12. printf("hello, %s”, "world");  


 

巨集的"##"符號

"##"符號的作用是在可變引數的個數為0時,消除引數前面的逗號:

#define MY_PRINTF(fs, …) printf(fs, ##__VA_ARGS__)

我們這樣呼叫:

MY_PRINTF(“Hello, World”);

等價於

printf(“Hello, World”);

另外"##"符號還能夠去掉括號,但是我現在還不是很明白,為什麼能夠做到這一點:

 

[cpp] view plaincopy

 

  1. #define ENUM_COTENTS(...) __VA_ARGS__   
  2. #define ENUM_CONTENT_REMOVE_PARENTHESIS(a) ENUM_COTENTS##a   
  3. #define DEFINE_ENUM(name) enum name { ENUM_CONTENT_REMOVE_PARENTHESIS(ENUM_LIST) };   
  4. #define ENUM_LIST (Sunday=1,Monday=2)   
  5. DEFINE_ENUM(WeekDay)  

巨集的"#"符號

"#"符號的作用是“字元化”程式碼:

#define MY_STRINGLIZED_MACRO(str) #str
int helloWorld = 0;
printf(MY_STRINGLIZED_MACRO(helloWorld)); // output: helloWorld

利用C++巨集實現簡單的.NET列舉型別

我做了一個簡單的用例,最終示例程式碼如下:

[cpp] view plaincopy

 

  1. #include "DefineEnum.h"   
  2. #define ENUM_LIST                                   \   
  3.         ENUM_NAME(Sunday     ENUM_VALUE(10)),       \   
  4.         ENUM_NAME(Monday     ENUM_VALUE(Sunday+1)),     \   
  5.         ENUM_NAME(Tuesday    ENUM_VALUE(123)),      \   
  6.         ENUM_NAME(Wednesday  ENUM_VALUE(10)) ,      \   
  7.         ENUM_NAME(Thursday   ENUM_VALUE(7)),        \   
  8.         ENUM_NAME(Friday     ENUM_VALUE(8)),        \   
  9.         ENUM_NAME(Saturday   ENUM_VALUE(12))   
  10.     
  11. DEFINE_ENUM(WeekDay);   
  12.     
  13. #include "RegisterEnum.h"   
  14. REGISTER_ENUM(WeekDay);   
  15.     
  16.     
  17. int main()   
  18. {   
  19.     printf("%s is %d.", EnumHelper<WeekDay>::ToString(Monday), Monday);   
  20.     getchar();   
  21.     return 0;   
  22. }  

DefineEnum.h

[cpp] view plaincopy

 

  1. #undef ENUM_LIST   
  2.     
  3. #undef ENUM_NAME   
  4. #define ENUM_NAME(...)  __VA_ARGS__   
  5.     
  6.     
  7. #undef ENUM_VALUE   
  8. #define ENUM_VALUE(val) = val   
  9.     
  10. #define ENUM_COTENTS(...)  __VA_ARGS__   
  11.     
  12. #define DEFINE_ENUM(name)  enum name { ENUM_COTENTS(ENUM_LIST) };  

RegisterEnum.h

[cpp] view plaincopy

 

  1. #include "ReflectEnum.h"   
  2.     
  3. #undef ENUM_VALUE   
  4. #define ENUM_VALUE(val)   
  5.     
  6. #define REGISTER_ENUM(name)  REFLECT_ENUM(name, ENUM_LIST )  

ReflectEnum.h

[cpp] view plaincopy

 

  1. #ifndef REFLECT_ENUM_INCLUDE_GUARD   
  2.     
  3. #include <string>   
  4. #include <cstring>   
  5. #include <stdexcept> // for runtime_error   
  6.     
  7. #endif   
  8.     
  9. template <typename Enum_T> class EnumHelper   
  10. {   
  11. public:   
  12.     static const char * ToString(Enum_T e)   
  13.     {   
  14.         for(int i = 0; i < _countof(EnumHelper<Enum_T>::s_allEnums); i++)   
  15.         {   
  16.             if( s_allEnums[i] == e)   
  17.                 return s_allEnumNames[i];   
  18.         }   
  19.         return NULL;   
  20.     }   
  21.     
  22. private:   
  23.     static const char * s_typeName;   
  24.     static Enum_T s_allEnums[];   
  25.     static char s_singleEnumStr[];   
  26.     static const char * s_allEnumNames[];   
  27.     
  28.     static void SplitEnumDefString()   
  29.     {   
  30.         char * p = s_singleEnumStr;   
  31.         while( isspace(*p) ) p++;   
  32.         for(int i = 0; i < _countof(EnumHelper<Enum_T>::s_allEnums); i++)   
  33.         {   
  34.             s_allEnumNames[i] = p;   
  35.             while( *p == '_' || isdigit(*p) || isalpha(*p) ) p++;   
  36.             bool meet_comma = ( *p == ',' );   
  37.             *p++ = '\0';   
  38.             if( !meet_comma )   
  39.             {   
  40.                 while( *p && *p != ',') p++;   
  41.                 if( *p ) p++;   
  42.             }   
  43.             while( *p && isspace(*p) ) p++;   
  44.         }   
  45.     }   
  46. };   
  47.     
  48. #define TO_ENUM_ITEM(...)  __VA_ARGS__   
  49. #define STRINGIZE(...)  #__VA_ARGS__   
  50.     
  51. #define REFLECT_ENUM(enum_type_name, enum_list)                                                                         \   
  52. template<> enum_type_name EnumHelper<enum_type_name>::s_allEnums[] =                                                    \   
  53. {                                                                                                                       \   
  54.     TO_ENUM_ITEM(enum_list)                                                                                             \   
  55. };                                                                                                                      \   
  56. template<> const char* EnumHelper<enum_type_name>::s_allEnumNames[_countof(EnumHelper<enum_type_name>::s_allEnums)];  \   
  57. template<> char EnumHelper<enum_type_name>::s_singleEnumStr[] = STRINGIZE(enum_list) ;                                  \   
  58. template<> const char * EnumHelper<enum_type_name>::s_typeName = (EnumHelper<enum_type_name>::SplitEnumDefString(), #enum_type_name); 

 

如果你問一個IT人士“C++如何實現類似Java的反射?”,結果會怎樣呢?~!@#¥%……&*,估計大部分人都會要稍微思考了一下,或者直接說“C++根本就不支援反射的呀!”。

是的,C++語言本身是不支援反射的,但實際應用中總是會有將物件序列化的需求,總不可能C++不支援,我們就不用C++了,既然發明C++的大師們沒有考慮這個,那我們只有自己動手了,毛主席說過“自己動手,豐衣足食”!

天生限制

C++語言本身不支援反射機制,但C++物件總是要序列化的,序列化就是儲存到磁碟上,將物件變成一定格式的二進位制編碼,然後要用的時候再將儲存在磁碟上的二進位制編碼轉化成一個記憶體中的物件,這個過程中總是需要有一個指示來告訴編譯器要生成什麼樣的物件,最簡單的方式當然就是類名了,例如:將一個ClassXXX物件儲存到磁碟上,再從磁碟讀取的時候讓編譯器根據“ClassXXX”名稱來new一個物件。

但是問題出現了,C++語言本身不支援反射,也就是說不能通過如下方式生成一個物件:

ClassXXX object = new “ClassXXX”;

 

工廠方法

當然,這樣的方法不行,那我們只有另闢蹊徑。最簡單的就是工廠方法了:

ClassXXX* object = FactoryCreate(“ClassXXX”);

至於FactoryCreate的設計就很簡單了,if的集合就可以了:

if(name = “ClassXXX”)

return new ClassXXX;

if(name = “ClassYYY”)

return new ClassYYY;

 

看起來不錯,來個類名就可以生成對應的物件,功能上解決了根據類名生成物件的問題。

假如以上所有的程式碼都有你一個人編寫,那當然問題不大,但是假如有一天你的公司擴大了,這部分程式碼由兩個不同的組A和B來維護,啊哈,問題來了,A組每新增或者修改一個類,都要通知B組更新FactoryCreate函式,也就是說A組的任何關於類的修改,都需要B組來修改,但實際上B的修改不產生任何價值,而且不勝其煩,永無止盡!!如果哪天來了一個新員工,由於對這個規定還不清楚,忘記了通知,那就完了:編譯通不過!

一個公司內都會產生如此多的問題,更何況微軟這樣的大公司是面對全球的各種各樣的客戶,如果微軟把這部分做進框架程式碼中,呵呵,那微軟所有的人不用幹其他事情了,每天處理來自全球的要求修改FactoryCreate函式的郵件和電話就夠他們忙的了:)

 

回撥工廠

既然此路不好走,那麼我們再考慮其它方法吧,一個可選的方法是將FactoryCreate做成回撥函式,框架提供註冊介面RegisterFactoryCreate,框架函式如此實現:

typedef CObject* (*FactoryCreate_PTR)(String name);

RegisterFactoryCreate(FactoryCreate_PTR fc_ptr);

應用程式碼如此實現:

CObject* MyFactoryCreate(String name);

RegisterFactoryCreate(MyFactoryCreate);

到這裡一個框架和應用分離的反射機制基本實現了,你是否長吁一口氣,然後準備泡杯咖啡,稍微放鬆一下呢?確實可以稍微休息一下了,畢竟我們完成了一件非常了不起的事情,讓C++實現了反射。

 

但你只悠閒了一兩天,麻煩事就來了。員工張三跑來向你抱怨“老大,李四註冊的反射函式把我的覆蓋了”!哦,你仔細一看,My god,這個註冊函式只能註冊一個反射函式,後註冊的就把前面的覆蓋了!

怎麼辦?總不可能又要求所有的類的反射函式都在一個工廠裡實現吧,那這樣就又回到了工廠方法中描述的時代了。

當然,聰明的你估計很快就能想出問題的解決方法,將RegisterFactoryCreate函式稍加修改就能滿足要求了,新的實現如下:

RegisterFactoryCreate(FactoryCreate_PTR fc_ptr,String className)

然後要求每個類都單獨寫自己的FactoryCreate_PTR函式,類似如下方式:

static CObject* ClassXXX::CreateClassXXX (){

return new ClassXXX;

};

 

static CObject* ClassYYY::CreateClassYYY(){

return new ClassYYY;

};

 

到此為此終於大功告成,通過我們的智慧實現了C++的反射功能!一股自豪感油然升起:)

 

最後的殺手鐗:巨集

當你為自己的聰明才智而驕傲的時候,那邊卻有幾個開發的兄弟在發出抱怨“唉,這麼多相似的函式,看著都眼花,每個類都要寫,煩死了”。

或者有一天,你要在每個類的CreateClass函式中增加一個其它功能(例如日誌),那麼開發的兄弟真的是要煩“死了”!!!

 

其實仔細一看,包括函式申明、函式定義、函式註冊,每個類的程式碼除了類名外其它都是一模一樣的,有沒有簡單的方法呢?

肯定是有的,這個方法就是巨集了,按照如下方法定義巨集:

[cpp] view plaincopy

 

  1. #define DECLARE_CLASS_CREATE(class_name) \  
  2.   
  3. static CObject* CreateClass## class_name ();  
  4.   
  5.   
  6. #define IMPL_CLASS_CREATE(class_name) \  
  7.   
  8. static CObject* CreateClass## class_name (){ \  
  9.   
  10. return new class_name; \  
  11.   
  12. };  
  13.   
  14.   
  15. #define REG_CLASS_CREATE(class_name) \  
  16.   
  17. RegisterFactoryCreate(class_name::CreateClass## class_name, #class_name);  


 

注:##是連線符,將兩個字串連線起來,#是將class_name作為字串處理。

 

大家可以比較一下,用了巨集和不用巨集是不是程式碼感覺完全不一樣呢?而且那天需要增加一個簡單的功能,只需要改巨集定義就ok了,不要全文搜尋所有相關函式,然後一個一個的重複新增。

 

到這裡才真正是大功告成!!

 

後記

某天分析Spring的IOC時,看到Digester最後利用的實際上是Java的反射機制來根據XML檔案定義生成Java物件,突發奇想:如果是C++該怎麼辦?

於是自己就開始分析起來,分析了一段時間突然想起微軟的MFC不正是要支援C++物件序列化的嗎?

趕緊開啟深入淺出MFC,重新將這部分研究了一下。看到微軟的天才們在MFC中用巨集來實現RTTI、Dynamic Create、Seralize功能時,我反過來思考“如果是我,我會如何設計?”、“為什麼會這麼設計”?然後一一分析這些各種可能的實現方式,一步一步的推導,最後發現竟然自然而然的就推出了MFC的這種實現方式!

當然,MFC的實現程式碼和我給出的程式碼不一樣(註冊方式不一樣),但設計思想是一樣的,各位看官可以自行稍加分析就明白了。

MFC的詳細實現可以參考侯捷的《深入淺出MFC》。

 

轉載自:http://blog.csdn.net/wangweitingaabbcc/article/details/7916963

作者部落格主頁:http://www.cnblogs.com/lizhanwu/ 本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利.