不思議な繰り返しのテンプレートパターン

ソフトウェア設計パターン

奇妙な繰り返しテンプレートパターンCRTP )は、もともとC++で登場した慣用句で、Xクラステンプレートのインスタンス化から、それ自体をテンプレート引数として用いてクラスを派生させるものですX[1]より一般的にはF境界多態性として知られ、 F境界量化の一種です

歴史

この手法は1989年に「F制限量化」として公式化されました。[2]「CRTP」という名前は、1995年にジム・コプリエンによって独自に造られました。 [3]彼は、初期のC++テンプレートコードの一部や、ティモシー・バッドがマルチパラダイム言語Ledaで作成したコード例でこの手法を観察していました。[4]これは、異なる基底クラスを置き換えることでクラス階層を拡張できるため、 「逆継承」と呼ばれることもあります。 [5] [6]

アクティブテンプレートライブラリ(ATL)におけるCRTPのMicrosoft実装は、同じく1995年にJan Falkinによって独自に発見されました。彼は誤って派生クラスから基底クラスを派生させていました。Christian BeaumontはFalkinのコードを初めて目にし、当初は当時のMicrosoftコンパイラではコンパイルできないと考えました。しかし、実際に動作することが分かり、Beaumontはこの誤りを基にATLとWindowsテンプレートライブラリ(WTL)の設計全体を設計しました。[要出典]

一般的な形式

// 奇妙な繰り返しテンプレート パターン (CRTP) 
template < typename T > class Base { // Base 内のメソッドはテンプレートを使用して Derived のメンバーにアクセスできます};  
  
    


クラスDerived : public Base < Derived > { // ... };     
    

[説明が必要]

このパターンの使用例としては、静的ポリモーフィズムや、 Andrei AlexandrescuがModern C++ Design解説しているようなメタプログラミング技術などが挙げられます[7]また、データ、コンテキスト、インタラクションパラダイム のC++実装においても重要な役割を果たしています[8] さらに、CRTPはC++標準ライブラリでこのstd::enable_shared_from_this機能を実装するために使用されています。[9]

静的ポリモーフィズム

通常、基本クラス テンプレートは、メンバー関数本体 (定義) が宣言されてからかなり経過するまでインスタンス化されないという事実を活用し、キャストを使用して、派生クラスのメンバーを独自のメンバー関数内で使用します。:

テンプレート< typename T > struct Base { void call () { // ... static_cast < T *> ( this ) -> implementation (); // ... }   
  
      
        
        
        
    

    static void staticFunc () { // ... T :: staticSubFunc (); // ... } };   
        
        
        
    


構造体Derived : public Base < Derived > { void implementation () { // ... }     
      
        
    

    静的void staticSubFunc () { // ... } };   
        
    

上記の例では、関数 はBase<Derived>::call()の存在がコンパイラによって認識される前(つまり、が宣言される前)に宣言されていますが、の宣言後に発生するコードによって実際に呼び出されるまで(上記の例には示されていません)、コンパイラによって実際にインスタンス化されることはありません。そのため、関数 がインスタンス化された時点では、 の宣言が認識されています。 struct DerivedDerivedDerivedcallDerived::implementation()

この手法は、仮想関数の使用と同様の効果を、動的ポリモーフィズムのコスト(およびある程度の柔軟性)なしで実現します。CRTPのこの特定の使用法は、「シミュレートされた動的バインディング」と呼ばれることもあります。[10]このパターンは、Windows ATLおよびWTLライブラリで広く使用されています。

上記の例を詳しく説明するために、仮想関数を持たない基底クラスを考えてみましょう。基底クラスが他のメンバー関数を呼び出すときは常に、自身の基底クラスの関数を呼び出します。この基底クラスからクラスを派生させると、オーバーライドされていないすべてのメンバー変数とメンバー関数(コンストラクタやデストラクタは含まない)が継承されます。派生クラスが継承した関数を呼び出し、その関数がさらに別のメンバー関数を呼び出す場合、その関数は派生クラス内の派生またはオーバーライドされたメンバー関数を呼び出すことはありません。

ただし、基底クラスのメンバ関数がすべてのメンバー関数呼び出しにCRTPを使用する場合、派生クラスでオーバーライドされた関数はコンパイル時に選択されます。これにより、サイズや関数呼び出しのオーバーヘッド(VTBL構造体メソッド参照、多重継承VTBL機構)を犠牲にすることなく、コンパイル時に仮想関数呼び出しシステムを効果的にエミュレートできますが、実行時にこの選択を行うことができないという欠点があります。

オブジェクトカウンター

オブジェクトカウンタの主な目的は、特定のクラスのオブジェクトの作成と破棄の統計情報を取得することです。[11]これはCRTPを使うことで簡単に解決できます。

template < typename T > class Counter { protected : // オブジェクトはこの型のポインターを通じて削除されるべきではありません~ Counter () { -- objectsAlive ; } public : static inline int objectsCreated = 0 ; static inline int objectsAlive = 0 ;  
  

    
     
        
    

         
         

    Counter () { ++ objectsCreated ; ++ objectsAlive ; } Counter ( const Counter & ) { ++ objectsCreated ; ++ objectsAlive ; } }; 
        
        
    
    
      
        
        
    


クラスX : public Counter <X> { // ... } ;     
    


クラスY : public Counter <Y> { // ... } ;     
    

クラスのオブジェクトXが作成されるたびに、 のコンストラクタがCounter<X>呼び出され、作成されたオブジェクトと生存しているオブジェクトのカウントがインクリメントされます。クラスのオブジェクトが破棄されるたびに、生存しているオブジェクトのカウントがデクリメントされます。と は別々のクラスであるXことに注意することが重要です。これが、 と のカウントが別々に保持される理由です。このCRTPの例では、このクラスの区別がテンプレートパラメータ()の唯一の使用法であり、単純なテンプレート化されていない基底クラスを使用できない理由です。 Counter<X>Counter<Y>XYTCounter<T>

多態的連鎖

メソッドチェーニング(名前付きパラメータイディオムとも呼ばれる)は、オブジェクト指向プログラミング言語において複数のメソッド呼び出しを行うための一般的な構文です。各メソッドはオブジェクトを返すため、中間結果を格納するための変数を必要とせずに、単一のステートメントでメソッド呼び出しを連鎖させることができます。

名前付きパラメータオブジェクトパターンをオブジェクト階層に適用すると、問題が発生する可能性があります。例えば、次のような基底クラスがあるとします。

std :: endlを使用します。std :: ostream使用します 
 

クラス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 ; } };  

     

      
         
 
      
        
           
          
    
 
      
       
             
          
    

プリントは簡単に連結できます:

プリンター( myStream ). println ( "hello" ). println ( 500 );

ただし、次の派生クラスを定義すると:

std :: coutを使用します 

enumクラスColor : char { RED ORANGE YELLOW GREEN BLUE INDIGO VIOLET };     
    
    
    
    
    
    
    


クラスCoutPrinter :パブリックプリンター{ public : CoutPrinter () :プリンター( 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クラスから継承します

クラスSquare : public Shape < Square > { // ... };     
    


クラスCircle : public Shape < Circle > { // ... };     
    

これにより、正方形、円、またはその他の図形のコピーを取得できますshapePtr->clone()

落とし穴

AbstractShape静的ポリモーフィズムにおける問題点の1つは、上記の例のような汎用的な基底クラスを使用しないと、派生クラスを均一に格納できない、つまり同じ基底クラスから派生した異なる型を同じコンテナに格納できないことです。例えば、はクラスではなく、特殊化を必要とするテンプレートであるstd::vector<Shape*>ため、 として定義されたコンテナは機能しません。 として定義されたコンテナには しか格納できず、 は格納できません。これは、CRTP基底クラスから派生した各クラスがそれぞれ固有の型であるためです。この問題の一般的な解決策は、上記の例のように、仮想デストラクタを持つ共有基底クラスを継承し、を作成できるようにすることですShapestd::vector<Shape<Circle>*>CircleSquareShapeAbstractShapestd::vector<AbstractShape*>

これを推論すると

CRTPの使用は、これを推論するC++23の機能を使用して簡素化できます[13] [14]関数が派生メンバー関数を呼び出すためにはがテンプレート化された型である必要があり、から継承して、その型をテンプレートパラメータとして渡す必要があります。 signatureDish()cookSignatureDish()ChefBaseCafeChefChefBase

テンプレート< typename T >クラスChefBase { public : void signatureDish () { static_cast < T *> ( this ) -> cookSignatureDish (); } };  
  

      
        
    


クラスCafeChef : public ChefBase < CafeChef > { public : void cookSignatureDish () { // ... } };     

      
        
    

明示的なオブジェクトパラメータを使用する場合、ChefBaseテンプレート化は不要で、そのままCafeChef派生できますChefBaseselfパラメータは自動的に正しい派生型として推定されるため、キャストは不要です。

クラスChefBase { public : template < typename Self > void signatureDish ( this Self && self ) { self . cookSignatureDish (); } };  

      
        
        
    


クラスCafeChef : public ChefBase { public : void cookSignatureDish () { // ... } };     

      
        
    

参照

参考文献

  1. ^ Abrahams, David ; Gurtovoy, Aleksey (2005年1月). C++ テンプレートメタプログラミング: Boost とその先の概念、ツール、テクニック. Addison-Wesley. ISBN 0-321-22725-5
  2. ^ William Cook他 (1989). 「オブジェクト指向プログラミングのためのF境界多態性」(PDF) .
  3. ^ Coplien, James O. (1995年2月). 「奇妙な繰り返しのテンプレートパターン」. C++レポート: 24–27 .
  4. ^ Budd, Timothy (1994). Ledaにおけるマルチパラダイムプログラミング. Addison-Wesley. ISBN 0-201-82080-3
  5. ^ “Apostate Café: ATL and Upside-Down Inheritance”. 2006年3月15日. 2006年3月15日時点のオリジナルよりアーカイブ。 2016年10月9日閲覧{{cite web}}: CS1 maint: bot: 元のURLステータス不明(リンク
  6. ^ “ATL and Upside-Down Inheritance”. 2003年6月4日. 2003年6月4日時点のオリジナルよりアーカイブ。 2016年10月9日閲覧{{cite web}}: CS1 maint: bot: 元のURLステータス不明(リンク
  7. ^ Alexandrescu, Andrei (2001). 『Modern C++ Design: Generic Programming and Design Patterns Applied』 . Addison-Wesley. ISBN 0-201-70431-5
  8. ^ ジェームズ・コプリエン;ビョルンヴィグ、ゲルトルート (2010)。リーン アーキテクチャ: アジャイル ソフトウェア開発用。ワイリー。ISBN 978-0-470-68420-7
  9. ^ "std::enable_shared_from_this" . 2022年11月22日閲覧
  10. ^ “Simulated Dynamic Binding”. 2003年5月7日. 2012年2月9日時点のオリジナルよりアーカイブ。 2012年1月13日閲覧
  11. ^ マイヤーズ、スコット(1998年4月)「C++におけるオブジェクトのカウント」C/C++ユーザーズジャーナル
  12. ^ Arena, Marco (2012年4月29日). 「ポリモーフィック連鎖にはCRTPを使用する」 . 2017年3月15日閲覧
  13. ^ ギャシュパー・アジュマン; Syブランド;ベン・ディーン。バリー・レヴジン(2021年7月12日)。 「これを推測します」。
  14. ^ 「明示的なオブジェクトパラメータ」 。 2023年12月27日閲覧
「https://en.wikipedia.org/w/index.php?title=Curiously_recurring_template_pattern&oldid=1318031825」から取得