CRTP per evitare il polimorfismo dinamico


Risposte:


139

Ci sono due modi.

Il primo consiste nello specificare staticamente l'interfaccia per la struttura dei tipi:

template <class Derived>
struct base {
  void foo() {
    static_cast<Derived *>(this)->foo();
  };
};

struct my_type : base<my_type> {
  void foo(); // required to compile.
};

struct your_type : base<your_type> {
  void foo(); // required to compile.
};

Il secondo consiste nell'evitare l'uso dell'idioma riferimento a base o puntatore a base ed eseguire il cablaggio in fase di compilazione. Utilizzando la definizione di cui sopra, puoi avere funzioni del modello simili a queste:

template <class T> // T is deduced at compile-time
void bar(base<T> & obj) {
  obj.foo(); // will do static dispatch
}

struct not_derived_from_base { }; // notice, not derived from base

// ...
my_type my_instance;
your_type your_instance;
not_derived_from_base invalid_instance;
bar(my_instance); // will call my_instance.foo()
bar(your_instance); // will call your_instance.foo()
bar(invalid_instance); // compile error, cannot deduce correct overload

Quindi combinare la definizione della struttura / interfaccia e la deduzione del tipo in fase di compilazione nelle funzioni consente di eseguire l'invio statico invece di quello dinamico. Questa è l'essenza del polimorfismo statico.


15
Ottima risposta
Eli Bendersky

5
Vorrei sottolineare che not_derived_from_basenon è derivato da base, né è derivato da base...
sinistra intorno al

3
In realtà, la dichiarazione di foo () all'interno di my_type / your_type non è richiesta. codepad.org/ylpEm1up (causa l'overflow dello stack) - C'è un modo per applicare una definizione di foo in fase di compilazione? - Ok, ho trovato una soluzione: ideone.com/C6Oz9 - Forse vuoi correggerlo nella tua risposta.
cooky451

3
Potresti spiegarmi qual è la motivazione per utilizzare CRTP in questo esempio? Se bar fosse definito come template <class T> void bar (T & obj) {obj.foo (); }, quindi qualsiasi classe che fornisce foo andrebbe bene. Quindi, in base al tuo esempio, sembra che l'unico uso di CRTP sia quello di specificare l'interfaccia in fase di compilazione. È a questo che serve?
Anton Daneyko

1
@Dean Michael In effetti il ​​codice nell'esempio viene compilato anche se foo non è definito in my_type e your_type. Senza questi override, base :: foo viene chiamato ricorsivamente (e stackoverflows). Quindi forse vuoi correggere la tua risposta come ha mostrato cooky451?
Anton Daneyko

18

Ho cercato io stesso discussioni decenti su CRTP. Todd Veldhuizen's Techniques for Scientific C ++ è una grande risorsa per questa (1.3) e molte altre tecniche avanzate come i modelli di espressione.

Inoltre, ho scoperto che potresti leggere la maggior parte dell'articolo C ++ Gems originale di Coplien su Google Books. Forse è ancora così.


@fizzer ho letto la parte che mi suggerisci, ma ancora non capisco cosa fa il template <class T_leaftype> double sum (Matrix <T_leaftype> & A); ti compra rispetto al modello <class Wwhat> double sum (Wenever & A);
Anton Daneyko

@AntonDaneyko Quando viene chiamato su un'istanza di base, viene chiamata la somma della classe di base, ad esempio "area di una forma" con implementazione predefinita come se fosse un quadrato. L'obiettivo di CRTP in questo caso è risolvere l'implementazione più derivata, "area di un trapezio" ecc. Pur essendo ancora in grado di riferirsi al trapezio come una forma fino a quando non è richiesto un comportamento derivato. Fondamentalmente, ogni volta che normalmente avresti bisogno dynamic_casto metodi virtuali.
John P

1

Ho dovuto cercare CRTP . Dopo averlo fatto, tuttavia, ho trovato alcune cose sul polimorfismo statico . Sospetto che questa sia la risposta alla tua domanda.

Si scopre che ATL utilizza questo modello in modo abbastanza estensivo.


-5

Questa risposta di Wikipedia ha tutto ciò di cui hai bisogno. Vale a dire:

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

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

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

Anche se non so quanto ti compri effettivamente. L'overhead di una chiamata di funzione virtuale è (dipendente dal compilatore, ovviamente):

  • Memoria: un puntatore a funzione per funzione virtuale
  • Runtime: una chiamata al puntatore di funzione

Mentre il sovraccarico del polimorfismo statico CRTP è:

  • Memoria: duplicazione di base per istanza di modello
  • Runtime: una chiamata al puntatore di funzione + qualunque cosa stia facendo static_cast

4
In realtà, la duplicazione dell'istanza di Base per template è un'illusione perché (a meno che tu non abbia ancora una vtable) il compilatore fonderà l'archiviazione della base e del derivato in un'unica struttura per te. Anche la chiamata del puntatore a funzione è ottimizzata dal compilatore (la parte static_cast).
Dean Michael

19
A proposito, la tua analisi di CRTP non è corretta. Dovrebbe essere: Memoria: niente, come ha detto Dean Michael. Runtime: una chiamata di funzione statica (più veloce), non virtuale, che è il punto centrale dell'esercizio. static_cast non fa nulla, consente solo la compilazione del codice.
Frederik Slijkerman,

2
Il punto è che il codice di base verrà duplicato in tutte le istanze del modello (la stessa fusione di cui parli). Simile ad avere un modello con un solo metodo che si basa sul parametro del modello; tutto il resto è migliore in una classe base altrimenti viene inserito ("unito") più volte.
user23167

1
Ogni metodo nella base verrà compilato nuovamente per ogni derivato. Nel caso (atteso) in cui ogni metodo istanziato è diverso (poiché le proprietà di Derived sono diverse), ciò non può essere necessariamente considerato come overhead. Ma può portare a una dimensione complessiva del codice maggiore, rispetto alla situazione in cui un metodo complesso nella classe base (normale) chiama metodi virtuali di sottoclassi. Inoltre, se metti metodi di utilità in Base <Derived>, che in realtà non dipendono affatto da <Derived>, verranno comunque istanziati. Forse l'ottimizzazione globale lo risolverà in qualche modo.
greggo

Una chiamata che passa attraverso diversi livelli di CRTP si espanderà nella memoria durante la compilazione ma può facilmente contrarsi tramite TCO e inlining. CRTP stesso non è davvero il colpevole, giusto?
John P
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.