2013-12-23 56 views
5

我想分析實現多態的各種方法之間的權衡。我需要一個具有一些相似性和成員函數差異的對象列表。我看到的選項如下:c + +交換與成員函數指針與虛擬繼承

  1. 在每個對象中都有一個標誌,並且在每個函數中都有一個switch語句。 該標誌的值將每個對象指向其每個函數的特定部分 。
  2. 在對象中有一個成員函數指針數組,這些成員函數指針在構造時分配爲 。然後,我把那個函數指針調到 得到正確的成員函數。
  3. 有一個具有多個派生類的虛擬基類。其中一個 的缺點是,我的列表現在必須包含指針,而不是對象本身。

我的理解是,選項3中列表中的指針查找將花費比選項2的成員函數查找更長的時間,因爲確保了成員函數的接近度。

這些選項的一些優點/缺點是什麼?性能優於可讀性。 有沒有其他的多態性方法?

+3

1.可怕的,2.稍微可怕的,3.你最好的選擇,4.你需要多態嗎?通常你可以通過使用模板來解決這個問題...... – Nim

+4

「我的優先考慮是性能優於可讀性。」 - 大錯特錯。如此可怕的錯誤。即使你需要性能,爲什麼你手動重新實現虛擬功能,如果語言已經提供了它們(通過多態 - vtables任何人?)? – 2013-12-23 09:37:49

+4

「我的優先考慮是性能優於可讀性。」在你有權說出這個句子之前,你應該學習基準/簡介代碼。 –

回答

2

一體,實現更快的多態性的方法是通過CRTP idiom and static polymorphism

template<typename T> 
struct base 
{ 
    void f() 
    { 
     static_cast<T*>(this)->f_impl(); 
    } 
}; 

struct foo : public base<foo> 
{ 
    void f_impl() 
    { 
     std::cout << "foo!" << std::endl; 
    } 
}; 

struct bar : public base<bar> 
{ 
    void f_impl() 
    { 
     std::cout << "bar!" << std::endl; 
    } 
}; 

struct quux : public base<quux> 
{ 
    void f_impl() 
    { 
     std::cout << "quux!" << std::endl; 
    } 
}; 


template<typename T> 
void call_f(const base<T>& something) 
{ 
    something.f(); 
} 

int main() 
{ 
    foo my_foo; 
    bar my_bar; 
    quux my_quux; 

    call_f(my_foo); 
    call_f(my_bar); 
    call_f(my_quux); 
} 

此輸出:

富!
酒吧!
quux!

靜態多態performs far better than virtual dispatch,因爲編譯器知道哪個函數將在編譯時被調用,它可以內嵌一切

即使它提供了動態綁定,也不能以通用異構容器方式執行多態,因爲基類的每個實例都是不同的類型。
但是,這可以通過boost::any之類的東西來實現。

2

隨着switch聲明,如果你想添加一個新的類,那麼你需要無處不在類接通修改,這可能是在你的代碼庫的各個地方。您的代碼庫之外可能還有一些需要修改的地方,但也許您知道在這種情況下情況並非如此。

在每個成員中都有一個成員函數指針數組,唯一的缺點是你爲每個對象複製了這個內存。如果你知道只有一個或兩個「虛擬」功能,那麼這是一個不錯的選擇。

至於虛擬功能,你是對的,你必須堆分配它們(或手動管理內存),但它是最具擴展性的選項。

如果你不是可擴展的,那麼(1)或(2)可能是你最好的選擇。與往常一樣,要說明的唯一方法就是衡量。我知道許多編譯器會在某些情況下通過跳轉表來實現一個switch語句,這個跳轉表基本上與虛函數表一樣。對於少數case聲明,他們可能只使用二分查找分支。

措施!

+0

'至於虛擬功能,你是對的,你必須堆分配它們(或手動管理內存)'不正確。你只需要通過引用或指針來傳遞對象(或者,顯然,直接用它的真正的靜態類型來調用它)。對象如何分配完全不相關。我使用堆棧中分配的對象來做大量多態的東西。最糟糕的是Stroustrup在他的一個常見問題解答中有相同的錯誤陳述。我想這就是人們不斷重複這一點的地方。 –

3
  1. 在每個對象中都有一個標誌,每個函數中都有一個switch語句。標誌的值將每個對象指向其每個功能的特定部分

    好的,所以如果基於標誌的代碼很少變化,這可能是有意義的。 這最大限度地減少了必須適合緩存的(重複)代碼的數量,並避免了任何函數間接調用。在某些情況下,這些好處可能會超過switch語句的額外成本。

  2. 在對象中有一個成員函數指針數組,這些成員函數指針在構造時分配。然後,我將該函數指針稱爲獲取正確的成員函數

    您可以節省一個間接訪問(對於vtable),但也會使對象變大,因此不適合緩存。不可能說哪個將主導,所以你只需要簡介,但它不是一個明顯的勝利

  3. 有一個虛擬的基類與幾個派生類。其中一個缺點是,我的列表現在必須包含指針,而不是對象本身

    如果你的代碼路徑不夠完全分離它們是合理的,這是最乾淨的解決方案。如果您需要優化它,您可以使用專門的分配器來確保它們是順序的(即使在您的容器中不是順序的),也可以使用類似於Boost.Any的巧妙包裝將對象直接移動到容器中。你仍然會得到vtable的間接性,但是我更喜歡這個到#2,除非分析表明它確實是一個問題。

所以,有幾個問題你應該回答,然後才能做出決定:

  1. 多少代碼是共享的,以及有多少變化?
  2. 對象有多大,並且一個內聯函數指針表會實質上影響你的緩存未命中狀態?

而且,在您回答完這些問題後,您應該只是簡介一下。