2009-12-23 90 views
29

當在C++中實現多態行爲時,可以使用純虛擬方法,也可以使用函數指針(或函子)。例如異步回調可以實現:虛擬方法或函數指針

方法1

class Callback 
{ 
public: 
    Callback(); 
    ~Callback(); 
    void go(); 
protected: 
    virtual void doGo() = 0; 
}; 

//Constructor and Destructor 

void Callback::go() 
{ 
    doGo(); 
} 

所以使用回調這裏,你需要重寫杜高()方法來調用任何你想要的功能

方法2

typedef void (CallbackFunction*)(void*) 

class Callback 
{ 
public: 
    Callback(CallbackFunction* func, void* param); 
    ~Callback(); 
    void go(); 
private: 
    CallbackFunction* iFunc; 
    void* iParam; 
}; 

Callback::Callback(CallbackFunction* func, void* param) : 
    iFunc(func), 
    iParam(param) 
{} 

//Destructor 

void go() 
{ 
    (*iFunc)(iParam); 
} 

要在這裏使用回調方法,您需要創建一個函數指針,由Callback對象調用。

方法3

[這是通過我(安德烈亞斯)加入到問題;它不是由原始的海報]

template <typename T> 
class Callback 
{ 
public: 
    Callback() {} 
    ~Callback() {} 
    void go() { 
     T t; t(); 
    } 
}; 

class CallbackTest 
{ 
public: 
    void operator()() { cout << "Test"; } 
}; 

int main() 
{ 
    Callback<CallbackTest> test; 

    test.go(); 
} 

各有什麼實現的優點和缺點寫的?

+1

或'go()'可以是一個模板函數,它可以帶*函數指針或帶重載的operator()(void *)的類。 – 2009-12-23 20:23:50

+2

方法2是編譯器在實現方法1的過程中正在執行的操作。您不應該自己動手,因爲它有危險且容易出錯。 – 2009-12-23 20:38:39

+0

很酷,謝謝Andreas的方法3 – doron 2009-12-23 20:45:06

回答

12

方法1(虛擬函數)

  • 「+」「正確的方式++做在C
  • 「 - 」性能 - 「 一個新的類必須每次回調
  • 創建」 - 與函數指針相比​​,通過VF-Table的額外解引用。與Functor解決方案相比,兩個間接引用。

方法2(具有函數指針的類)

  • 「+」可以包裝用於C++回調類
  • 「+」回調函數C風格功能可以在創建回調對象之後改變
  • 「 - 」需要一個間接調用。對於可以在編譯時靜態計算的回調函數,可能比functor方法慢。

方法3(類叫牛逼仿函數)

  • 「+」 可能以最快的速度做到這一點的方式。沒有間接調用開銷並可能完全內聯。
  • 「 - 」需要定義一個附加的Functor類。
  • 「 - 」要求回調在編譯時靜態聲明。

FWIW,函數指針不一樣的函子。 Functors(用C++)是用來提供函數調用的類,它通常是operator()。

下面是一個例子算符以及其利用仿函數自變量的模板函數:

class TFunctor 
{ 
public: 
    void operator()(const char *charstring) 
    { 
     printf(charstring); 
    } 
}; 

template<class T> void CallFunctor(T& functor_arg,const char *charstring) 
{ 
    functor_arg(charstring); 
}; 

int main() 
{ 
    TFunctor foo; 
    CallFunctor(foo,"hello world\n"); 
} 

從性能角度來看,虛函數和函數指針都導致間接函數調用(即,通過一個寄存器),儘管虛擬函數在加載函數指針之前需要額外加載VFTABLE指針。使用Functors(使用非虛擬調用)作爲回調函數是使用參數來模板函數的最高性能方法,因爲它們可以內聯,即使沒有內聯,也不會生成間接調用。

+0

但是在我的方法2中,它們可以互換使用嗎? – doron 2009-12-23 20:32:05

+0

從技術上講,如果您將go()作爲類提供的功能而不是operator(),則所有三個類都可以鬆散地被視爲仿函數。 #2中的函數指針本身與函數無關。然而,只有方法3遵循真正的C++函數的「精神」,並且可以受益於通常被認爲是在C++中使用的函數的模板優化。 – Adisak 2009-12-23 21:08:35

6

方法1

  • 更容易閱讀和理解錯誤的
  • 可能性較小(iFunc,你不使用void *iParam
  • C++程序員會告訴你,這是不能爲空「正確」 的方式做到這一點在C++

方法2

  • 略少於打字做
  • 非常稍快(調用虛方法具有一些開銷,通常是相同的兩個簡單的算術運算的。所以它最有可能關係不大)
  • 那怎麼你會做在C

方法3

大概可能的情況下做到這一點的最好辦法。它將具有最佳性能,它將是類型安全的,並且很容易理解(這是STL使用的方法)。

+4

虛擬函數在防止內聯小函數,刪除重複或不可能的分支和/或使用僅寄存器變量存儲時,速度可能會超過40倍。 – 2009-12-23 20:31:16

+0

@贊:#1也防止內聯優化,這就是我也在比較它。現在我還添加了選項#3 – 2009-12-23 20:34:09

+0

爲什麼方法2是在C++中執行此操作的「正確」方法有兩個原因1. C++引入了函子,它在某種意義上擴展了函數指針的思想。 2.在實現創建新線程時,Boost線程庫採取方法2。 – doron 2009-12-23 20:35:20

0

函數指針是更多的C風格,我會說。主要是因爲要使用它們,您通常必須定義一個與指針定義具有相同確切簽名的扁平函數。

當我編寫C++時,我寫的唯一的扁平函數是int main()。其他一切都是類對象。在兩種選擇中,我會選擇定義一個類並覆蓋你的虛擬,但如果你想要的是通知某些代碼在你的類中發生了某些動作,那麼這兩個選擇都不是最好的解決方案。

我不知道您的具體情況,但你可能要仔細閱讀design patterns

我建議觀察者模式。這是當我需要監視課程或等待某種通知時使用的。

+0

Adisak對函子的說法是個好主意,雖然我沒有用太多 – Charles 2009-12-23 20:30:06

1

第一種方法的一個主要優點是它具有更多的類型安全性。第二種方法使用void *作爲iParam,因此編譯器將無法診斷類型問題。

第二種方法的一個小優點是與C集成的工作量會小一些。但是如果您的代碼庫只有C++,那麼這個優點是沒有意義的。

2

從您的示例中不清楚您是否創建實用程序類。你是回調類旨在實現一個封閉或更實質的對象,你只是沒有充實?

第一種形式:

  • 更容易閱讀和理解,
  • 遠遠易於擴展:嘗試添加方法暫停,恢復停止
  • 更好地處理封裝(假設doGo在類中定義)。
  • 可能是更好的抽象,所以更容易維護。

第二種形式:

  • 可以用不同的方法可用於杜高,所以它不僅僅是多態多。
  • 可能允許(通過其他方法)在運行時更改doGo方法,允許該對象的實例在創建後突變其功能。

最終,IMO,第一種形式對所有正常情況都更好。第二個有一些有趣的功能 - 但不是你經常需要的功能。

0

例如,讓我們看看一個接口,用於添加功能類:

struct Read_Via_Inheritance 
{ 
    virtual void read_members(void) = 0; 
}; 

任何時候,我想補充的閱讀另一個來源,我必須從類繼承並添加一個特定的方法:

struct Read_Inherited_From_Cin 
    : public Read_Via_Inheritance 
{ 
    void read_members(void) 
    { 
    cin >> member; 
    } 
}; 

如果我想從文件,數據庫或USB讀取,這需要3個單獨的類。隨着多個對象和多個來源,組合開始變得非常難看。

如果我使用一個函子,這恰好類似於訪問者設計模式:

struct Reader_Visitor_Interface 
{ 
    virtual void read(unsigned int& member) = 0; 
    virtual void read(std::string& member) = 0; 
}; 

struct Read_Client 
{ 
    void read_members(Reader_Interface & reader) 
    { 
    reader.read(x); 
    reader.read(text); 
    return; 
    } 
    unsigned int x; 
    std::string& text; 
}; 

利用上述的基礎上,對象可以從不同的來源僅僅通過向read_members方法供給不同的讀者閱讀:

struct Read_From_Cin 
    : Reader_Visitor_Interface 
{ 
    void read(unsigned int& value) 
    { 
    cin>>value; 
    } 
    void read(std::string& value) 
    { 
    getline(cin, value); 
    } 
}; 

我不必更改任何對象的代碼(這是件好事,因爲它已經在工作)。我也可以將讀者應用於其他對象。

通常,我在執行泛型編程時使用繼承。例如,如果我有Field班,則可以創建Field_BooleanField_TextField_Integer。在可以把他們的實例指向一個vector<Field *>並稱之爲一個記錄。記錄可以在字段上執行通用操作,並且不關心或知道處理字段的類型種類

0
  1. 更改爲純虛擬,首先關閉。然後將其內聯。只要內聯不會失敗(如果強制它,它就不會失效),否則應該否定任何方法開銷調用。
  2. 也可以使用C,因爲這是C++與C相比唯一真正有用的主要特性。您將始終調用方法並且它不能被內聯,所以效率會更低。
5

方法2的主要問題是它不能縮放。考慮100個函數的等價物:

class MahClass { 
    // 100 pointers of various types 
public: 
    MahClass() { // set all 100 pointers } 
    MahClass(const MahClass& other) { 
     // copy all 100 function pointers 
    } 
}; 

MahClass的大小已經膨脹,並且構建它的時間也顯着增加。然而,虛擬函數是O(1)增加了類型的構建時間 - 更不用說用戶必須手動編寫所有派生類的所有回調函數,從而調整指針成爲一個派生指針,並且必須指定函數指針類型和一個混亂。更不要說你可能會忘記一個,或者將它設置爲NULL或者同樣愚蠢的想法,但是完全會發生,因爲你以這種方式編寫了30個類,並且像寄生蜂一樣違反了毛蟲。

只有當所需的回調是靜態可知時,方法3纔可用。

這使得方法1成爲唯一可用的方法,當需要動態方法調用時。

+0

如果複製函數指針成爲一個問題,函數指針可能被抽象爲一個單獨的靜態表,這就是v表通常如何操作。 – doron 2012-06-28 10:08:36