奇妙な繰り返しテンプレートパターン( CRTP ) は、元々はC++の慣用表現で、クラスが自身をテンプレート引数として使用してXクラステンプレートのインスタンス化から派生するものです。 [ 1 ]より一般的にはF 境界多相性として知られており、 F境界量化の一形態です。X
この手法は 1989 年に「F境界量化」として形式化されました。[ 2 ]「CRTP」という名称は、1995 年にJim Coplienによって独自に考案されました。 [ 3 ]彼は、初期のC++テンプレート コードや、Timothy Budd がマルチパラダイム言語 Leda で作成したコード例の中にこの手法を見出しました。[ 4 ]異なる基底クラスを置き換えることでクラス階層を拡張できることから、「逆継承」と呼ばれることもあります。 [ 5 ] [ 6 ]
マイクロソフトのActive Template Library (ATL)におけるCRTPの実装は、同じく1995年にJan Falkinによって独自に発見されました。彼は派生クラスから偶然にも基底クラスを派生させてしまったのです。Christian BeaumontはFalkinのコードを初めて見たとき、当時利用可能だったマイクロソフトのコンパイラではコンパイルできないだろうと考えました。しかし、実際にコンパイルできることが判明すると、Baumontはこのミスを基にATLとWindows Template Library(WTL)全体の設計を構築しました。
// 奇妙な繰り返しパターン (CRTP) template < typename T > class Base { // Base 内のメソッドは、テンプレートを使用して Derived のメンバーにアクセスできます};class Derived : public Base < Derived > { // ... };このパターンのユースケースとしては、静的ポリモーフィズムや、Andrei Alexandrescuが『Modern C++ Design』で説明しているような他のメタプログラミング技術などがあります。[ 7 ]また、データ、コンテキスト、インタラクションのパラダイム の C++ 実装でも重要な役割を果たしています。 [ 8 ] さらに、CRTP は C++ 標準ライブラリでstd::enable_shared_from_this機能の実装に使用されています。[ 9 ]
通常、基底クラスのテンプレートは、メンバ関数の本体(定義)が宣言後かなり経ってからインスタンス化されるという事実を利用し、キャストを使用して派生クラスのメンバを自身のメンバ関数内で使用します。例:
template < typename T > struct Base { void call () { // ... static_cast < T *> ( this ) -> implementation (); // ... }static void staticFunc () { // ... T :: staticSubFunc (); // ... } };struct Derived : public Base < Derived > { void implementation () { // ... }static void staticSubFunc () { // ... } };上記の例では、関数はBase<Derived>::call()、コンパイラがの存在を認識する前(つまり、が宣言される前)に宣言されていますが、実際には、の宣言後に発生する後続のコードによって実際に呼び出されるまで、コンパイラによってインスタンス化されません(上記の例では示されていません)。そのため、関数がインスタンス化される時点では、の宣言が知られています。struct DerivedDerivedDerivedcallDerived::implementation()
この手法は、動的多態性のコスト(およびある程度の柔軟性)なしに、仮想関数の使用と同様の効果を実現します。CRTP のこの特定の使用法は、一部の人によって「シミュレートされた動的バインディング」と呼ばれています。[ 10 ]このパターンは、Windows ATLおよびWTLライブラリで広く使用されています。
上記の例をさらに詳しく説明すると、仮想関数を持たない基底クラスを考えてみましょう。基底クラスが別のメンバ関数を呼び出す場合、必ず自身の基底クラスの関数が呼び出されます。この基底クラスから派生クラスを作成すると、オーバーライドされていないメンバ変数とメンバ関数(コンストラクタやデストラクタを除く)がすべて継承されます。派生クラスが継承された関数を呼び出し、その関数がさらに別のメンバ関数を呼び出す場合、その関数は派生クラス内の派生またはオーバーライドされたメンバ関数を一切呼び出しません。
ただし、基底クラスのメンバ関数がすべてのメンバ関数呼び出しにCRTPを使用する場合、派生クラスでオーバーライドされた関数はコンパイル時に選択されます。これにより、コンパイル時に仮想関数呼び出しシステムを効果的にエミュレートできますが、サイズや関数呼び出しのオーバーヘッド(VTBL構造体、メソッド検索、多重継承VTBL機構)のコストは発生しません。ただし、実行時にこの選択を行うことができないという欠点があります。
オブジェクトカウンタの主な目的は、特定のクラスのオブジェクトの作成と破棄の統計情報を取得することです。[ 11 ]これはCRTPを使用して簡単に解決できます。
template < typename T > class Counter { protected : static inline int objectsCreated = 0 ; static inline int objectsAlive = 0 ;// オブジェクトはこの型のポインタを介して削除されるべきではありません~ Counter () { -- objectsAlive ; } public : Counter () { ++ objectsCreated ; ++ objectsAlive ; } Counter ([[ maybe_unused ]] const Counter & x ) { ++ objectsCreated ; ++ objectsAlive ; } };class X : public Counter < X > { // ... };class Y : public Counter < Y > { // ... };クラスのオブジェクトXが作成されるたびに、のコンストラクタがCounter<X>呼び出され、作成済みカウントと生存カウントの両方が増加します。クラスのオブジェクトがX破棄されるたびに、生存カウントが減少します。Counter<X>と はCounter<Y>2 つの別々のクラスであるため、それぞれXとのカウントを別々に保持しますY。この CRTP の例では、このクラスの区別がテンプレート パラメータ (TのCounter<T>) の唯一の使用例であり、単純なテンプレートなしの基底クラスを使用できない理由です。
メソッドチェーン(名前付きパラメータイディオムとも呼ばれる)は、オブジェクト指向プログラミング言語で複数のメソッド呼び出しを行う際によく用いられる構文です。各メソッドはオブジェクトを返すため、中間結果を格納するための変数を用意することなく、単一のステートメントでメソッド呼び出しを連鎖させることができます。
名前付きパラメータオブジェクトパターンをオブジェクト階層に適用すると、問題が発生する可能性があります。次のような基底クラスがあるとします。
using std :: endl ; using std :: ostream ;class Printer { private : ostream & stream ; public : explicit Printer ( ostream & pstream ) : stream { pstream } {} template < typename T > Printer & print ( T && t ) { stream << t ; return * this ; } template < typename T > Printer & println ( T && t ) { stream << t << endl ; return * this ; } };プリントは簡単に連鎖させることができます。
Printer ( myStream ) .println ( "hello" ). println ( 500 );しかし、次の派生クラスを定義すると、
using std :: cout ;enum class Color : char { RED , ORANGE , YELLOW , GREEN , BLUE , INDIGO , VIOLET };class CoutPrinter : public Printer { public : CoutPrinter () : Printer ( cout ) {}CoutPrinter & setConsoleColor ( Color c ) { // ... return * this ; } };基底クラスの関数を呼び出すとすぐに、具体的なクラスは「失われる」。
// v----- ここには「Printer」があり、「CoutPrinter」はありません。 CoutPrinter (). print ( "Hello " ). setConsoleColor ( Color :: RED ). println ( "Printer!" ); // コンパイルエラーこれは、「print」が基本クラスである「Printer」の関数であり、そして「Printer」インスタンスを返すためです。
CRTP は、このような問題を回避し、「ポリモーフィック チェイニング」を実装するために使用できます。[ 12 ]
std :: ostreamを使用します。// 基底クラスtemplate < typename ConcretePrinter > class Printer { private : ostream & stream ; public : explicit Printer ( ostream & pstream ) : stream { pstream } {} template < typename T > ConcretePrinter & print ( T && t ) { stream << t ; return static_cast < ConcretePrinter &> ( * this ); } template < typename T > ConcretePrinter & println ( T && t ) { stream << t << std :: endl ; return static_cast < ConcretePrinter &> ( * this ); } };enum class Color : char { // ここに色を記述}; // 派生クラスclass CoutPrinter : public Printer < CoutPrinter > { public : CoutPrinter () : Printer ( cout ) {} CoutPrinter & setConsoleColor ( Color c ) { // ... return * this ; } }; // 使用例CoutPrinter (). print ( "Hello " ). setConsoleColor ( Color :: RED ). println ( "Printer!" );ポリモーフィズムを使用する場合、基底クラスポインタを使用してオブジェクトのコピーを作成する必要が生じることがあります。このための一般的な手法は、派生クラスごとに定義された仮想クローン関数を追加することです。CRTPを使用すると、その関数や同様の関数を派生クラスごとに複製する必要がなくなります。
std :: unique_ptrを使用します。// 基底クラスにはクローンを作成するための純粋仮想関数がありますclass AbstractShape { public : virtual ~ AbstractShape () = default ; virtual unique_ptr < AbstractShape > clone () const = 0 ; };// この CRTP クラスは Derived の clone() を実装しますtemplate < typename Derived > class Shape : public AbstractShape { protected : // Shape クラスが継承される必要があることを明確にしますShape () = default ; Shape ( const Shape & ) = default ; Shape ( Shape && ) = default ; public : unique_ptr < AbstractShape > clone () const override { return std :: make_unique < Derived > ( static_cast < Derived const &> ( * this )); } };// すべての派生クラスは抽象クラスではなくCRTPクラスを継承しますclass Square : public Shape < Square > { // ... };class Circle : public Shape < Circle > { // ... };これにより、正方形、円、またはその他の形状のコピーを取得できますshapePtr->clone()。
AbstractShape静的ポリモーフィズムの問題の 1 つは、上記の例のように汎用基底クラスを使用しないと、派生クラスを均一に格納できないことです。つまり、同じ基底クラスから派生した異なる型を同じコンテナに格納できません。たとえば、として定義されたコンテナは、がクラスではなく特殊化が必要なテンプレートであるstd::vector<Shape*>ため機能しませんShape。として定義されたコンテナは、std::vector<Shape<Circle>*>のみを格納できCircle、Squareを格納できません。これは、CRTP 基底クラスから派生した各クラスがShape一意の型であるためです。この問題に対する一般的な解決策は、上記の例のように仮想デストラクタを持つ共有基底クラスから継承し、AbstractShapeを作成できるようにすることですstd::vector<AbstractShape*>。
CRTP の使用は、 C++23で導入された明示的なオブジェクト パラメータ(通称「推論」) を使用することで簡略化できます。[ 13 ] [ 14 ]関数が派生メンバ関数を呼び出すには、テンプレート型である必要があり、から継承し、その型をテンプレート パラメータとして渡す必要があります。thissignatureDish()cookSignatureDish()ChefBaseCafeChefChefBase
template < typename T > class ChefBase { public : void signatureDish () { static_cast < T *> ( this ) -> cookSignatureDish (); } };class CafeChef : public ChefBase < CafeChef > { public : void cookSignatureDish () { // ... } };明示的なオブジェクトパラメータを使用する場合、ChefBaseテンプレート化は不要で、そのままCafeChef派生させることができますChefBase。selfパラメータは自動的に適切な派生型として推論されるため、キャストは不要です。
class ChefBase { public : template < typename Self > void signatureDish ( this Self && self ) { self.cookSignatureDish ( ); } } ;class CafeChef : public ChefBase { public : void cookSignatureDish () { // ... } };{{cite web}}: CS1 maint: bot: 元の URL の状態が不明です (リンク){{cite web}}: CS1 maint: bot: 元の URL の状態が不明です (リンク)