c++實現反射類


  在很多程序設計中,經常會遇到這樣的需求,即可以通過類的名字得到對應類型的對象,尤其是一種數據需要很多策略處理的時候。比如對於網頁類型的識別,一篇網頁可能是視頻類型、新聞類型、圖片類型、網站首頁、百科等很多類型中的一種,網頁類型對於搜索引擎來說是非常重要的,計算rank的時候網頁類型往往是一個非常重要的因子。具體實現的時候,網頁類型識別的策略可以封裝在類中,這樣一個策略就可以設計成一個類。但是后期隨着對網頁理解的越來越深入,就會出現以下兩種情景:

  1. 需要添加新的網頁類型,因此需要添加對應的類型識別類;
  2. 有些類型已經不再需要或者是進行了重新划分,那么需要刪除掉這些類型或者是讓這些類型識別模塊不再生效。
  這種應用場景下,添加或移除網頁類型識別模塊時,最好能夠非常方便,並且不會影響到已有的程序。
  一個比較好的方案是,定義一個類型識別的基類PageTypeDetector,每個類型識別策略都繼承自這個基類。比如需要一個新聞頁識別的新策略,那么定義類NewsPageTypeDetector,該類繼承PageTypeDetector。在添加NewsPageTypeDetector到網頁類型識別的主程序時,在配置文件中進行配置,添加NewsPageTypeDetector類,讓該類生效,而主程序和其他類型識別策略的程序都不需要進行改動。另外,如果不再需要圖片網頁類型識別,那么就把圖片類型識別對應的類名直接從配置發文件中刪除即可。
  為了實現上述目標,我們需要從類名到類型的映射,可以稱為反射。因為配置文件中的信息在程序內部得到的都是純字符串,程序需要根據字符串生成對應的識別類。當然,這個在本身已包含反射機制的程序設計語言中很容易實現,比如JAVA,但是由於C++中語言本身不支持這種機制,因此,需要用其他的方法來模擬這種機制。
  首先,我們從最簡單的方式開始,定義一個工廠方法,該方法負責根據類名生成相應類的對象,函數定義可以如下:
  PageTypeDetector* DetectorFactoryCreate(const string& class_name);

  生成新聞網頁類型識別的類可以如下調用:

  PageTypeDetector* news_page_detector = DetectorFactoryCreate("NewsPageTypeDetector");

  DetectorFactoryCreate工廠方法中的實現邏輯大致是這樣:

  if (class_name == "NewsDocTypeDetector") {
      return new NewsDocTypeDetector;
  } else if (class_name == "...") {
      return new ...;
  }

  使用如上工廠方法創建類的方式具有非常明顯的缺陷,每添加或刪除一個新類,都需要修改工廠方法內的程序(添加if判斷或者刪除if判斷,並且需要添加新類的頭文件或者類聲明),當然了,因為程序有了修改所以就需要重新編譯(如果很多其他模塊依賴該程序的話,重新編譯也是一筆不小的開銷)。顯然,這種方式雖然簡單,但是極不易於維護。

  這里,提出一個使用非常方便並且易於維護的解決方案,那就是使用宏。雖然c++創始人 Bjarne Stroustrup極力反對使用宏,但是在一些特定的場景中合理的使用宏會帶來意想不到的效果。
  首先,從使用宏最簡單的一個實現開始,目標是可以通過類的名字得到相應的對象,因此應該有個方法類似於如下:
   Any GetInstanceByName(const string& class_name);

  返回值為Any,因為不知道返回值究竟是什么類型,所以假定可以返回任何類型,這里的Any使用的是Boost中的Any。該方法中需要new一個類型為class_name的對象返回,那么應該如何new該對象呢?借用上面使用工廠方法的經驗,可以進一步使用工廠類,對於每個類,都有一個相應的工廠類ObjectFactoryClassName,由該工廠類負責生成相應的對象(為什么要使用工廠類?后面再作簡單介紹)。

  有了工廠類,也需要將類名與工廠類對應起來,對應方式可以使用map<string, ObjectFactory*> object_factory_map,object_factory_map負責從類名到相應工廠類的映射,這樣,就可以通過類的名字找到對應ObjectFactory,然后使用ObjectFactory生成相應的對象。但是如何將相應的工廠類添加到object_factory_map中去呢,我們需要在定義新類的時候就將對應的工廠類添加到object_factory_map中,這里需要一個函數負責添加工廠類到object_factory_map中去(為什么需要一個函數負責?最后作簡單說明)。

  負責將新類對應的工廠類添加到全局變量object_factory_map的函數必須在使用object_factory_map之前執行。gcc中有一個關鍵字__attribute__((constructor)) ,使用該關鍵字聲明的函數就可以在main函數之前執行。到現在,程序的結構類似這樣:

 

// 負責實現反射的文件reflector.h:

map<string, ObjectFactory*> object_factory_map;
Any GetInstanceByName(const string& name) {
    if (object_factory_map.find(name) != object_factory_map.end()) {
        return object_factory_map[name]->NewInstance();
    }
    return NULL;
}

#define REFLECTOR(name) \
class ObjectFactory##name { \ 
public: \
  Any NewInstance() { \
    return Any(new name); \
    } \
}; \
void register_factory_##name() { \
    if (object_factory_map.find(#name) == object_factory_map.end()) { \
      object_factory_map[#name] = new ObjectFactory##name(); \
    } \
} \
__attribute__(constructor)void register_factory##name();


// 調用文件test.cc
class TestClass {
public:
  void Out() {
    cout << "i am TestClass" << endl;
  }
};
REFLECTOR(TestClass);

// main函數
int main() {
  Any instance = GetInstanceByName("TestClass");
  TestClass* test_class = instance.any_cast<TestClass>();
  return 0;  
}

 

  到這里還有一個問題,全局變量ObjectFactoryMap是不能放在頭文件中的,因為如果多個類包含該頭文件時,就會出現重復定義的錯誤,是編譯不過的。因此,將該變量放在其源碼reflector.cc文件中:

// reflector.h,包含聲明:
extern map<string, ObjectFactory*> object_factory_map;
Any GetInstanceByName(const string& name);

// reflector.cc:
map<string, ObjectFactory*> object_factory_map;
Any GetInstanceByName(const string& name) {
    if (object_factory_map.find(name) != object_factory_map.end()) {
        return object_factory_map[name]->NewInstance();
    }
    return NULL;
}

  上述程序編譯能夠通過,但是運行時出錯,后來定位到是在使用全局變量object_factory_map時出錯,經過調試了很久,在網上查相應的資料也沒找到。經過不停的嘗試,才發現原來是全局變量object_factory_map沒有初始化,在仔細的測試了以后發現,是__attribute__((constructor))與全局變量類構造函數的執行順序的問題,一般全局變量是在__attribute__(constructor)前完成初始化的,但是如果__attribute__是在main函數所在的文件,而全局變量是在其他文件定義的,那么__attribute__(constructor)就會在全局變量類構造函數前面執行,這樣,上面的程序在全局變量類還沒有完成初始化,也就是還沒有執行構造函數,就在__attribute__(constructor)聲明的函數中進行了使用,因此會出現問題。不過,在執行__attribute__時已經看到了全局變量的定義,只是沒有執行全局變量的構造函數(這里,如果全局變量不是類,而是普通類型,是沒有問題的)。所以,程序的結構還需要進一步修改。

    現在解決如何定義和使用全局變量object_factory_map的問題。既然我們不能直接使用該變量,那么可以通過顯示調用函數來返回該變量,如果直接在函數中new一個對象返回的話,那么每次調用都會new一個新的對象,而我們全局只需要一個該對象,這時該是static出現的時候了。我們可以這樣定義:
// reflector.cc
map<string, ObjectFactory*>& object_factory_map() {
    static map<string, ObjectFactory*>* factory_map = new map<string, ObjectFactory*>;
    return *factory_map;
}

這樣定義還有另外一個優點,程序只是在真正需要調用g_objectfactory_map時才會生成相應的對象,而如果程序沒有調用,也不會生成對應的對象。當然,在這里new一個對象的代價不大,但是如果new的對象非常耗時的話,這種使用函數中static變量代替全局變量方法的優勢就非常明顯了。到現在反射程序變成如下這樣:

// 負責實現反射的文件reflector.h:

// 工廠類的基類
class ObjectFactory {
 public:
  virtual Any NewInstance() {
    return Any(); 
  }
};

map<string, ObjectFactory*>& object_factory_map();
Any GetInstanceByName(const string& name);

#define REFLECTOR(name) \
class ObjectFactory##name : public ObjectFactory { \ 
 public: \
  Any NewInstance() { \
    return Any(new name); \
  } \
}; \
void register_factory_##name() { \
    if (object_factory_map().find(#name) == object_factory_map().end()) { \
      object_factory_map()[#name] = new ObjectFactory##name(); \
    } \
} \
__attribute__(constructor)void register_factory##name()



// reflector.cc

map<string, ObjectFactory*>& object_factory_map() {
    static map<string, ObjectFactory*>* factory_map = new map<string, ObjectFactory*>;
    return *factory_map;
}

Any GetInstanceByName(const string& name) {
    if (object_factory_map().find(name) != object_factory_map().end()) {
        return object_factory_map()[name]->NewInstance();
    }
    return NULL;
}

  到現在接近尾聲了,不過在很多時候,我們都是在已有基類的基礎上添加新的類,就好比上述網頁識別的程序,各個識別策略類都繼承共同的基類,這樣,我們可以進一步修改反射程序,將GetInstanceByName放在另外一個類中,返回的是基類的指針,因此在定義基類時也需要注冊一個宏,如下所示,同時需要修改objector_factory_map的結構為map<string, map<string, ObjectFactory> >,第一個key是基類的名字,第二map中的key是生成類的名字,基類宏的定義類似如下:

#define REFLECTOR_BASE(base_class) \
class base_class##Reflector { \
 public: \
  static base_class* GetInstanceByName(const string& name) { \
     map<string, ObjectFactory*>& map = object_factory_map()[#base_class]; \
     map<string, ObjectFactory*>::iterator iter = map.find(name); \
     if (iter == map.end()) { \
       return NULL; \
     } \
      Any object = iter->second->NewInstance(); \
      return *(object.any_cast<base_class*>()); \
} \
};

  這里就不再詳細講修改后的代碼了,有興趣的朋友可以自己實現。

 

注:

  至於上面為什么需要使用工廠類,而不是直接new一個對應的對象返回,原因是直接new是不可以的。例如如下定義:
#define REFLECT(name) \
Any GetInstanceByName(const string& class_name) {
    return Any(new name);
}

  如果是多個類使用的話,那么就會出現多個函數的定義。如果也借助工廠類的實現,如下實現:

#define REFLECT(name) \
Any GetInstanceByName##name(const string& class_name) {
    return Any(new name);
}

   這樣是不會出現重復定義了,但是這樣在生產新的對象時需要指定特定的函數,這不又回到原點了嗎?因此工廠類充當的是個中介的角色,我們可以保存工廠類,然后根據名稱尋找特定的工廠類來生成對應的對象。

注:

 為什么需要使用函數添加工廠類?因為在程序中,全局空間中只能是變量的聲明和定義,而不能是語句,例如:

可以這樣寫:
int a = 10;
int main() {}
但是不能這樣寫:
int a;
a = 10;
int main() {}
 

需要注意的知識點:

  1. 工廠模式;
  2. 全局變量的定義需要注意,不能定義在頭文件中(當如,如果經過特殊處理,例如使用#ifndef保護另說);
  3. Any類型的實現;(准備寫另外一篇文章來探討其實現細節)
  4. 宏的定義以及使用;(基本覆蓋了宏的所有知識)
  5. 全局變量構造函數與__attribute__((constructor))的執行順序;(調試了很久)
  6. __attribute__((constructor))的問題;(編譯器有關,放在函數定義前或定義后)
  7. 全局空間只能是聲明或者定義,不能是語句;
  8. static在函數中的使用;
  9. 全局變量類的定義與使用。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM