risorse | crtp – curiously recurring template pattern
Il Curiously Recurring Template Pattern (espressione coniata da James Coplien[1]) è un idioma del C++ secondo il quale una classe C deriva da una classe template istanziata sulla classe C stessa:
template <class T> class B { }; class C : public B<C> { };
Il CRTP viene frequentemente presentato come implementazione del polimorfismo statico, un meccanismo polimorfico risolto a compile-time:
#include <iostream> template <class T> class B { public: void f() { static_cast<T*>(this)->f(); } }; class C : public B<C> { public: void f() { std::cout << "C::f" << std::endl; } }; int main() { B<C>* b = new C; b->f(); delete b; return 0; } /* output: * * C::f */
A fronte di una minor leggibilità del codice, si ottiene un aumento delle prestazioni, venendo meno la risoluzione del metodo C::f attraverso la v-table tipica del polimorfismo dinamico dei metodi virtuali. Con la stessa tecnica si possono rendere polimorfici pure i metodi statici:
#include <iostream> template <class T> class B { public: static void f() { T::f(); } }; class C : public B<C> { public: static void f() { std::cout << "C::f" << std::endl; } }; int main() { B<C>::f(); return 0; } /* output: * * C::f */
Se la classe derivata C non implementa tutti i metodi richiamati da B, si corre il rischio di uno stack overflow, in virtù del fatto che la chiamata static_cast<T*>(this)->f() in B::f viene risolta su sé stessa, causando una ricorsione infinita.
Scopo principale del CRTP è risolvere una dipendenza circolare tra due classi, usando l'ereditarietà in un senso, il tipo parametrico nell'altro. La classe derivata «vede» la base per le proprietà della derivazione, la classe base «vede» la derivata grazie al template. Il compilatore C++ gestisce agevolmente questa dipendenza circolare, fintanto che la struttura della classe base non dipende dalla classe derivata.
Un esempio di dipendenza circolare si può riscontrare nello sviluppo di un albero generico a partire da uno binario:
class BinaryTreeNode { public: typedef std::tr1::shared_ptr<BinaryTreeNode> Ptr; private: Ptr m_left; Ptr m_right; public: virtual ~BinaryTreeNode() { } Ptr left() const { return m_left; } void left(Ptr left) { m_left = left; } Ptr right() const { return m_right; }; void right(Ptr right) { m_right = right; } };
L'implementazione dell'albero generico necessita dei metodi left, right per organizzare, secondo la tradizione, i nodi figli nel sotto-albero di sinistra e i nodi fratelli in quello di destra. L'accesso a tali funzionalità avviene sfruttando l'ereditarietà – tralasciando in questo caso di considerare quale sia la forma più opportuna, pubblica o privata, e scegliendo la prima per semplicità:
class MultiwayTreeNode : public BinaryTreeNode { Ptr firstChild() const { return left(); } Ptr nextSibling() const { return right(); } void appendChild(Ptr node) { if (left()) appendSibling(left(), node); else left(node); } void appendSibling(Ptr node) { if (right()) appendSibling(right(), node); else right(node); } protected: void appendSibling(Ptr target, Ptr node) { while (target->right()) target = target->right(); target->right(node); } };
A questo punto ci si scontra con la dipendenza circolare: ci si attende infatti che i metodi firstChild e nextSibling ritornino dei puntatori a MultiwayTreeNode, piuttosto che puntatori alla classe più generica BinaryTreeNode. La soluzione si ottiene indicando esplicitamente a BinaryTreeNode la vera natura del tipo dei puntatori m_left e m_right per mezzo di un tipo parametrico, realizzando così il CRTP:
template <class T> class BinaryTreeNode { public: typedef std::tr1::shared_ptr<T> Ptr; private: Ptr m_left; Ptr m_right; public: virtual ~BinaryTreeNode() { } Ptr left() const { return m_left; } void left(Ptr left) { m_left = left; } Ptr right() const { return m_right; }; void right(Ptr right) { m_right = right; } }; class MultiwayTreeNode : public BinaryTreeNode<MultiwayTreeNode> { public: Ptr firstChild() const { return left(); } Ptr nextSibling() const { return right(); } void appendChild(Ptr node) { if (left()) appendSibling(left(), node); else left(node); } void appendSibling(Ptr node) { if (right()) appendSibling(right(), node); else right(node); } protected: void appendSibling(Ptr target, Ptr node) { while (target->right()) target = target->right(); target->right(node); } };
L'intervento produce, come effetto collaterale, la non-istanziabilità della classe base BinaryTreeNode; il problema si risolve introducendo una classe ausiliaria:
// rename BinaryTreeNode in BinaryTreeNodeImpl, then: class BinaryTreeNode : public BinaryTreeNodeImpl<BinaryTreeNode> { }; class MultiwayTreeNode : public BinaryTreeNodeImpl<MultiwayTreeNode> { // ...
Un tipico utilizzo del polimorfismo statico è l'emulazione di una chiamata virtuale nel costruttore:
#include <iostream> template <class T> class B { public: B() { static_cast<T*>(this)->f(); } void f() { std::cout << "B::f" << std::endl; } }; class C : public B<C> { public: void f() { std::cout << "C::f" << std::endl; } }; int main() { C c; return 0; } /* output: * * C::f */
L'uso del polimorfismo può portare alla necessità di clonare un oggetto del quale si possiede un riferimento o un puntatore alla classe base. Il problema viene normalmente risolto introducendo un metodo virtuale puro Clone nella classe base, demandando alle classi derivate la sua implementazione, che di norma si rifà direttamente al costruttore di copia:
#include <iostream> class B { public: virtual void f() { std::cout << "B::f" << std::endl; } virtual B* clone() = 0; }; class D1 : public B { public: virtual void f() { std::cout << "D1::f, this=" << this << std::endl; } virtual B* clone() { return new D1(*this); } }; class D2 : public B { public: virtual void f() { std::cout << "D2::f, this=" << this << std::endl; } virtual B* clone() { return new D2(*this); } }; int main() { B* d1 = new D1; B* d1clone = d1->clone(); d1->f(); d1clone->f(); B* d2 = new D2; B* d2clone = d2->clone(); d2->f(); d2clone->f(); delete d1; delete d1clone; delete d2; delete d2clone; return 0; } /* output: * * D1::f, this=00D081B8 * D1::f, this=00D081E8 * D2::f, this=00D08278 * D2::f, this=00D082A8 */
Il CRTP consente di rifattorizzare il metodo Clone in una classe intermedia, lasciando alle classi derivate la possibilità di sovrascriverlo solo se necessario:
#include <iostream> class B { public: virtual void f() { std::cout << "B::f" << std::endl; } virtual B* clone() = 0; }; template <class T> class CopyCtorBasedCloner : public B { public: virtual B* clone() { return new T(static_cast<const T&>(*this)); } }; class D1 : public CopyCtorBasedCloner<D1> { public: virtual void f() { std::cout << "D1::f, this=" << this << std::endl; } }; class D2 : public CopyCtorBasedCloner<D2> { public: virtual void f() { std::cout << "D2::f, this=" << this << std::endl; } }; int main() { B* d1 = new D1; B* d1clone = d1->clone(); d1->f(); d1clone->f(); B* d2 = new D2; B* d2clone = d2->clone(); d2->f(); d2clone->f(); delete d1; delete d1clone; delete d2; delete d2clone; return 0; } /* output: * * D1::f, this=002181B8 * D1::f, this=002181E8 * D2::f, this=00218278 * D2::f, this=002182A8 */
In generale, il CRTP può essere utile per estrapolare (in una classe base) una funzionalità condivisa da un insieme di classi (derivanti dalla nuova classe) la cui implementazione risulti parametrica sul tipo dell'oggetto sul quale si opera.
Il CRTP può essere usato per estendere un'interfaccia: è per esempio possibile implementare degli operatori relazionali per un'intera gerarchia di classi derivate che implementano i soli operatori operator< e operator==, ovvero gli operatori infissi a partire dagli operatori di assegnamento composto operator+=, operator-=, …:
#include <iostream> template <class T> class Comparable { public: bool operator<=(const T& c) const { const T& c_ = static_cast<const T&>(*this); return c_ < c || c_ == c; } // etc. }; class C : public Comparable<C> { int m_c; public: explicit C(int c) : m_c (c) { } bool operator<(const C& c) const { return m_c < c.m_c; } bool operator==(const C& c) const { return m_c == c.m_c; } }; int main() { C c1(1), c2(2); std::cout << "c1 is" << (c1 < c2 ? " " : " not ") << "less than c2" << std::endl; std::cout << "c2 is" << (c2 < c1 ? " " : " not ") << "less than c1" << std::endl; std::cout << "c1 is" << (c1 == c2 ? " " : " not ") << "equal to c2" << std::endl; std::cout << "c2 is" << (c2 == c1 ? " " : " not ") << "equal to c1" << std::endl; // won't compile unless C derives from Comparable<C> std::cout << "c1 is" << (c1 <= c2 ? " " : " not ") << "less than or equal to c2" << std::endl; std::cout << "c2 is" << (c2 <= c1 ? " " : " not ") << "less than or equal to c1" << std::endl; return 0; } /* output: * * c1 is less than c2 * c2 is not less than c1 * c1 is not equal to c2 * c2 is not equal to c1 * c1 is less than or equal to c2 * c2 is not less than or equal to c1 */
Il CRTP è anche noto come mix-in dall'alto. Un esempio di implementazione mix-in con il CRTP è disponibile qui. Relativamente all'uso del CRTP come mix-in, Scott Meyers ritiene che la scelta del nome CRTP per questo idioma sia piuttosto infelice, e propone – per quanto per sua stessa ammissione non ne faccia ampio uso nemmeno lui – Do It For Me[4].
Pagina modificata l'8/11/2011