2017-05-06 157 views
8

我正試圖圍繞CRTP包裹我的頭。有一些很好的資源,包括這個論壇,但我認爲我對靜態多態的基礎知識有些困惑。看着下面的維基百科條目:關於CRTP靜態多態性的困惑

template <class T> 
struct Base 
{ 
    void implementation() 
    { 
     // ... 
     static_cast<T*>(this)->implementation(); 
     // ... 
    } 

static void static_func() 
{ 
    // ... 
    T::static_sub_func(); 
     // ... 
    } 
}; 

struct Derived : public Base<Derived> 
{ 
    void implementation(); 
    static void static_sub_func(); 
}; 

我明白,這可以幫助我有不同的實現()的派生類變種,有點像一個編譯時虛函數。然而,我的困惑是,我想我不能有像

void func(Base x){ 
    x.implementation(); 
} 

功能我會與正常的繼承和虛函數,由於基數正在模板,但我必須要麼指定

func(Derived x) 

或使用

template<class T> 
func(T x) 

那麼,是什麼CRTP實際購買我在這方面,而不是簡單地遮蔽/實施中派生:: Base的簡單方法?

struct Base 
{ 
    void implementation(); 

struct Derived : public Base 
{ 
    void implementation(); 
    static void static_sub_func(); 
}; 
+0

當你把由值的'Base'例如,你從[對象切片]遭受(https://en.wikipedia.org/wiki/Object_slicing)。如果您想要多態行爲(無論使用靜態還是動態多態),請通過引用或指針傳遞它。 –

+0

不適用於我,我得到變量或字段'func'聲明爲void void func(Base&x)... – user32849

+0

@ user32849'Base'是一個模板。你不能只使用'Base&',你必須提供一個模板參數:'Base &'。這意味着'func'也必須是一個模板。 – Angew

回答

5

當涉及多個功能時,CRTP的優勢才變得明顯。考慮下面的代碼(沒有CRTP):

struct Base 
{ 
    int algorithm(int x) 
    { 
    prologue(); 
    if (x > 42) 
     x = downsize(x); 
    x = crunch(x); 
    epilogue(); 
    return x; 
    } 

    void prologue() 
    {} 

    int downsize(int x) 
    { return x % 42; } 

    int crunch(int x) 
    { return -x; } 

    void epilogue() 
    {} 
}; 

struct Derived : Base 
{ 
    int downsize(int x) 
    { 
    while (x > 42) x /= 2; 
    return x; 
    } 

    void epilogue() 
    { std::cout << "We're done!\n"; } 
}; 

int main() 
{ 
    Derived d; 
    std::cout << d.algorithm(420); 
} 

此輸出:

[Live example]

由於C++的類型系統的靜態特性,調用d.algorithm調用Base的所有功能。 Derived中的嘗試覆蓋未被調用。

這改變了在使用CRTP:

template <class Self> 
struct Base 
{ 
    Self& self() { return static_cast<Self&>(*this); } 

    int algorithm(int x) 
    { 
    self().prologue(); 
    if (x > 42) 
     x = self().downsize(x); 
    x = self().crunch(x); 
    self().epilogue(); 
    return x; 
    } 

    void prologue() 
    {} 

    int downsize(int x) 
    { return x % 42; } 

    int crunch(int x) 
    { return -x; } 

    void epilogue() 
    {} 
}; 

struct Derived : Base<Derived> 
{ 
    int downsize(int x) 
    { 
    while (x > 42) x /= 2; 
    return x; 
    } 

    void epilogue() 
    { std::cout << "We're done!\n"; } 
}; 

int main() 
{ 
    Derived d; 
    std::cout << d.algorithm(420); 
} 

輸出:

我們就大功告成了!
-26

[Live example]

這樣,在Base實施實際上將調入Derived每當Derived提供了一個 「覆蓋」。

這甚至會在您的原始代碼中可見:如果Base不是CRTP類,則其對static_sub_func的調用永遠不會解析爲Derived::static_sub_func


至於什麼CRTP的優於其他方法的優點是:

  • CRTP與virtual功能:

    CRTP是一個編譯時的結構,這意味着有關聯的運行時開銷。通過基類引用調用虛函數(通常)需要通過指向函數的指針進行調用,因此會產生間接成本並阻止內聯。

  • CRTP與簡單地Derived實施的一切:

    基類代碼的重用。

當然,CRTP是純粹的編譯時構造。爲了達到它允許的編譯時多態性,你必須使用編譯時多態結構:模板。有兩種方法可以做到這一點:

template <class T> 
int foo(Base<T> &actor) 
{ 
    return actor.algorithm(314); 
} 

template <class T> 
int bar(T &actor) 
{ 
    return actor.algorithm(314); 
} 

前者對應更緊密地運行時多態性,並提供更好的類型安全,後者是基於更鴨打字。

+0

這顯然是CRTP的一個很好的使用,但是我的問題和CRTP的描述一般是「靜態多態性」,CRTP與多態性沒有任何直接關係。這是一個幫助實施的工具。 –

+0

@NirFriedman它確實與多態性有關,但是*編譯時*多態性。這隻能通過模板來實現。我會在答案中加上一點。 – Angew

+0

我明白編譯時多態性是什麼。是的,你的代碼中有一個模板類。但CRTP並不是純粹根據其接口使用類型的函數,而是多態性。 Base僅僅是一個幫助實現Derived或類似類的工具。如果它是關於實現的幫助,那麼關於多態只是關於接口,並不是真的。 –

6

問題在於CRTP作爲「靜態多態性」的描述對於實際使用的CRPT並不真正有用或不準確。多態性實際上只是具有完成相同接口或合同的不同類型;這些不同類型如何實現該接口與多態性是正交的。動態多態性看起來是這樣的:

void foo(Animal& a) { a.make_sound(); } // could bark, meow, etc 

Animal是一個基類提供虛擬make_sound方法,即DogCat等,覆蓋。這裏是靜態多態:

template <class T> 
void foo(T& a) { a.make_sound(); } 

就是這樣。您可以調用任何類型的foo的靜態版本,該版本恰好定義了一個make_sound方法,而無需繼承基類。這個調用將在編譯時解決(即你不會爲一個vtable調用付費)。

那麼CRTP適合在哪裏? CRTP實際上並不是關於接口的,所以它不是關於多態的。 CRTP是讓你更容易實現的事情。 CRTP的神奇之處在於它可以將事物直接注入到類型的接口中,並充分了解派生類型提供的所有內容。一個簡單的例子可能是:

template <class T> 
struct MakeDouble { 
    T double() { 
     auto& me = static_cast<T&>(*this); 
     return me + me; 
}; 

現在定義加法運算符的任何類,也可以給予一個double方法:

class Matrix : MakeDouble<Matrix> ... 

Matrix m; 
auto m2 = m.double(); 

CRTP是所有關於實施幫助,而不是接口。所以不要太在意它通常被稱爲「靜態多態性」的事實。如果您想要了解可以使用CRTP的真正規範示例,請考慮Andrei Alexandrescu的Modern C++設計的第1章。雖然,慢慢來:-)。

+0

CRTP有多種用途。這是其中之一,但提問者所談論的也是一個有效的方法。 –

0

你是正確的,無論

void func(Base x); 

void func(Derived x); 

給你靜態的多態性。第一個不編譯,因爲Base不是一個類型,第二個不是多態。

但是,假設您有兩個派生類,Derived1Derived2。那麼,你可以做的是使func本身成爲一個模板。

template <typename T> 
void func(Base<T>& x); 

然後可以與任何類型從Base派生調用,它會使用任何的參數傳遞給決定哪些函數來調用靜態類型。


這只是CRTP的用途之一,如果我猜測我會說不太常見的一種。你也可以使用它作爲Nir Friedman在另一個答案中提出,這與靜態多態性沒有任何關係。

兩種用途進行了討論很好here