2009-07-08 116 views
27

想象一下,我有一堆C++相關的類(所有擴展了相同的基類和提供相同的構造函數),我在一個公共頭文件(我包括)中聲明,以及它們在一些其他文件中的實現(我編譯和靜態鏈接爲我的程序構建的一部分)。從名稱實例化類?

我想能夠實例化其中的一個傳遞名稱,這是一個必須傳遞給我的程序(作爲命令行或作爲編譯宏)的參數。

,唯一可能的解決方案我看到的是使用宏:

#ifndef CLASS_NAME 
#define CLASS_NAME MyDefaultClassToUse 
#endif 

BaseClass* o = new CLASS_NAME(param1, param2, ..); 

是它的唯一有價值的方法?

回答

39

這是使用常用解決的問題Registry Pattern

這就是 註冊模式描述的情況:

對象需要聯繫另一個 對象,只知道對象的名稱 或 提供的服務名稱,但不包括如何與其聯繫。 提供一個服務,該服務採用對象,服務或角色的名稱 ,並且 返回一個遠程代理,該代理將 與如何與 聯繫的知識聯繫到指定的對象。

這是基本相同的發佈/發現,形成服務 導向架構(SOA)的基礎模型 和 在OSGi的服務層。

您通常使用單例對象實現註冊表,在編譯時或啓動時通知單例對象的對象名稱以及構造它們的方式。然後您可以使用它來按需創建對象。

例如:

template<class T> 
class Registry 
{ 
    typedef boost::function0<T *> Creator; 
    typedef std::map<std::string, Creator> Creators; 
    Creators _creators; 

    public: 
    void register(const std::string &className, const Creator &creator); 
    T *create(const std::string &className); 
} 

您註冊對象的名稱和創建功能,像這樣:

Registry<I> registry; 
registry.register("MyClass", &MyClass::Creator); 

std::auto_ptr<T> myT(registry.create("MyClass")); 

然後,我們就用聰明的宏簡化這個以使其能夠在完成編譯時間。 ATL使用提供了可以在運行時通過名字來創建組件類註冊表模式 - 註冊是使用類似下面的代碼一樣簡單:

OBJECT_ENTRY_AUTO(someClassID, SomeClassName); 

這個宏被放置在某處你的頭文件,魔術它會導致在COM服務器啓動時在單例中註冊。

2

在C++中,這個決定必須在編譯時做出。

在編譯時你可以使用一個typedef,而不是玻璃陶瓷:

typedef DefaultClass MyDefaultClassToUse; 

這相當於,避免了宏(宏壞;-))。

如果要在運行期間做出決定,則需要編寫自己的代碼來支持它。 simples解決方案是一個函數,它測試字符串並實例化相應的類。

擴展版本(允許獨立代碼段註冊他們的類)將是map<name, factory function pointer>

+1

「擴展版本(允許獨立的代碼段註冊它們的類)將是一個映射<名稱,工廠函數指針>。」 從類定義中可以將自己註冊到該映射中,假設該映射位於其他地方的Factory中? 在Java中,我可以/使用靜態構造函數來完成它。實際上,我不想爲我編寫的每個新子類修改Factory的代碼。 – puccio 2009-07-08 08:15:09

5

爲什麼不使用對象工廠?

最簡單的形式:

BaseClass* myFactory(std::string const& classname, params...) 
{ 
    if(classname == "Class1"){ 
     return new Class1(params...); 
    }else if(...){ 
     return new ...; 
    }else{ 
     //Throw or return null 
    } 
    return NULL; 
} 
1

您提到了兩種可能性 - 命令行和彙編宏,但每種解決方案都有很大的不同。

如果選擇是通過編譯宏來完成的,那麼這是一個可以用#defines和#ifdefs等解決的簡單問題。您提出的解決方案與任何解決方案一樣好。

但是,如果選擇是在運行時使用命令行參數進行的,那麼您需要有一些能夠接收字符串並創建相應對象的Factory框架。這可以使用簡單的,靜態的if().. else if()... else if()...鏈來完成,該鏈具有所有可能性,或者可以是完全動態的框架,其中對象註冊並被克隆以提供自己的新實例。

10

實現此目的的一種方法是對類「名稱」映射到工廠函數進行硬編碼。模板可能會縮短代碼。 STL可以使編碼更容易。

#include "BaseObject.h" 
#include "CommonClasses.h" 

template< typename T > BaseObject* fCreate(int param1, bool param2) { 
    return new T(param1, param2); 
} 

typedef BaseObject* (*tConstructor)(int param1, bool param2); 
struct Mapping { string classname; tConstructor constructor; 
    pair<string,tConstructor> makepair()const { 
     return make_pair(classname, constructor); 
    } 
} mapping[] = 
{ { "class1", &fCreate<Class1> } 
, { "class2", &fCreate<Class2> } 
// , ... 
}; 

map< string, constructor > constructors; 
transform(mapping, mapping+_countof(mapping), 
    inserter(constructors, constructors.begin()), 
    mem_fun_ref(&Mapping::makepair)); 

編輯 - 在一般請求:)有點返工,使事情看起來更平滑(學分石免費誰沒有可能要添加一個答案本人)

typedef BaseObject* (*tConstructor)(int param1, bool param2); 
struct Mapping { 
    string classname; 
    tConstructor constructor; 

    operator pair<string,tConstructor>() const { 
     return make_pair(classname, constructor); 
    } 
} mapping[] = 
{ { "class1", &fCreate<Class1> } 
, { "class2", &fCreate<Class2> } 
// , ... 
}; 

static const map< string, constructor > constructors( 
     begin(mapping), end(mapping)); // added a flavor of C++0x, too. 
+0

Yeargs,最後的'變革'號召讓我的眼睛受傷。 % - ) – 2009-07-08 09:56:44

0

在過去,我已經實現了Factory模式,使得類可以在運行時進行自我註冊,而工廠本身不必具體瞭解它們。關鍵是要使用稱爲(IIRC)的「初始化附件」的非標準編譯器功能,其中您在實現文件中爲每個類聲明一個虛擬靜態變量(例如bool),並通過調用註冊來初始化它常規。

在這個方案中,每個類都必須#include包含它的工廠的頭文件,但工廠除了接口類之外什麼都不知道。你可以從你的版本中直接添加或者刪除實現類,並且不用修改代碼就可以重新編譯。

問題是隻有一些編譯器通過初始化支持連接-IIRC其他人在第一次使用時初始化文件範圍變量(與函數本地靜態工作方式相同),由於從不訪問虛擬變量,工廠地圖將始終爲空。

我感興趣的編譯器(MSVC和GCC)確實支持這一點,所以對我來說這不是問題。你必須自己決定這個解決方案是否適合你。

1

雖然現在問題已經存在了四年多了,但它仍然有用。因爲在編譯和鏈接主代碼文件時調用未知的新代碼是當今非常常見的情況。根本沒有提到這個問題的一個解決方案。因此,我喜歡將觀衆指向不是用C++構建的另一種解決方案。 C++本身沒有能力表現得像從Java已知的Class.forName()或從.NET已知的Activator.CreateInstance(type)。由於上述原因,虛擬機沒有對JIT代碼進行實時監控。但無論如何,LLVM,低級虛擬機,爲您提供所需的工具和庫以讀入編譯的庫。基本上,你需要執行兩個步驟:

  1. 編譯C/C++源代碼,你喜歡動態實例。你需要將它編譯成bitcode,所以你最終得到了一個,比如foo.bc。你可以用鏗鏘做到這一點,並提供一個編譯器開關:clang -emit-llvm -o foo.bc -c foo.c
  2. 你需要再使用ParseIRFile()方法從llvm/IRReader/IRReader.h解析foo.bc文件,以獲得相關的功能(LLVM本身只知道用作位碼是直接抽象對於更高級別的中間表示,比如Java字節碼,CPU操作碼並不太熟悉)。有關更完整的代碼說明,請參閱此article的實例。

在上面勾畫了這些步驟之後,您還可以從C++動態調用其他之前未知的函數和方法。