奇妙に繰り返されるテンプレートパターン(CRTP)とは何ですか?


187

本を参照せずに、誰でもCRTPコード例を使って良い説明を提供できますか?


2
SOでCRTPの質問を読みます:stackoverflow.com/questions/tagged/crtp。それはあなたにいくつかのアイデアを与えるかもしれません。
sbi 2010年

68
@sbi:彼がそうするなら、彼は彼自身の質問を見つけるでしょう。そして、それは不思議なことに繰り返されます。:)
クレイグ・マックイーン2013年

1
ところで、この言葉は「不思議なことに再帰的」であるべきだと私には思えます。意味を誤解していますか?
Craig McQueen

1
クレイグ:そうだと思います。複数のコンテキストで発生することが判明したという意味で、「不思議なことに繰り返し」です。
Gareth McCaughan

回答:


275

要するに、CRTPは、クラスAがそのクラスA自体のテンプレート特殊化である基本クラスを持っている場合です。例えば

template <class T> 
class X{...};
class A : public X<A> {...};

それはされて不思議なことではない、定期的な?:)

さて、これはあなたに何を与えますか?これにより、実際にXテンプレートはその特殊化の基本クラスになることができます。

たとえば、次のようなジェネリックシングルトンクラス(簡易バージョン)を作成できます。

template <class ActualClass> 
class Singleton
{
   public:
     static ActualClass& GetInstance()
     {
       if(p == nullptr)
         p = new ActualClass;
       return *p; 
     }

   protected:
     static ActualClass* p;
   private:
     Singleton(){}
     Singleton(Singleton const &);
     Singleton& operator = (Singleton const &); 
};
template <class T>
T* Singleton<T>::p = nullptr;

ここで、任意のクラスAをシングルトンにするには、これを行う必要があります

class A: public Singleton<A>
{
   //Rest of functionality for class A
};

ご覧のように?シングルトンテンプレートは、あらゆるタイプの特殊化Xが継承されることを前提としているためsingleton<X>GetInstance!CRTPには他にも便利な使い方があります。たとえば、クラスに現在存在するすべてのインスタンスをカウントしたいが、このロジックを別のテンプレートにカプセル化したい場合(具象クラスのアイデアは非常に単純です-静的変数を持ち、ctorでインクリメントし、dtorでデクリメントします)。練習としてやってみてください!

Boostのさらにもう1つの有用な例(私は彼らがどのようにそれを実装したかはわかりませんが、CRTPもそうします)。<クラスにはオペレーターのみを提供し、クラスには自動的にオペレーターを提供したいと想像してください==

あなたはこのようにそれを行うことができます:

template<class Derived>
class Equality
{
};

template <class Derived>
bool operator == (Equality<Derived> const& op1, Equality<Derived> const & op2)
{
    Derived const& d1 = static_cast<Derived const&>(op1);//you assume this works     
    //because you know that the dynamic type will actually be your template parameter.
    //wonderful, isn't it?
    Derived const& d2 = static_cast<Derived const&>(op2); 
    return !(d1 < d2) && !(d2 < d1);//assuming derived has operator <
}

これでこのように使えます

struct Apple:public Equality<Apple> 
{
    int size;
};

bool operator < (Apple const & a1, Apple const& a2)
{
    return a1.size < a2.size;
}

今、あなたは明示的に演算子==を提供していませんAppleか?しかし、あなたはそれを持っています!あなたは書ける

int main()
{
    Apple a1;
    Apple a2; 

    a1.size = 10;
    a2.size = 10;
    if(a1 == a2) //the compiler won't complain! 
    {
    }
}

これは、の演算子==を記述しただけの場合は少なく書くように見えるかもしれませんが、テンプレートが、だけでなく、などを提供するAppleと想像してEqualityください。そして、これらの定義を複数のクラスに使用して、コードを再利用できます!==>>=<=

CRTPは素晴らしいものです:) HTH


61
この投稿では、シングルトンを優れたプログラミングパターンとして推奨していません。シングルトンを一般的に理解できる図として使用しているだけです
。imothe

3
@Armen:回答はCRTPを明確に理解できる方法で説明しています。すばらしい回答です。このようなすばらしい回答に感謝します。
Alok Save

1
@Armen:この素晴らしい説明に感謝します。以前はCRTPを取得していましたが、平等の例が光っています!+1
ポール

1
CRTPのさらに別の使用例は、コピー不可能なクラスが必要な場合です。NonCopyable&operator =(const NonCopyable&); }; 次に、以下のようにnoncopyableを使用します。class Mutex:private NonCopyable <Mutex> {public:void Lock(){} void UnLock(){}};
Viren 14

2
@パピー:シングルトンはひどいものではありません。他のアプローチがより適切である場合、それは平均以下のプログラマーによってはるかに過剰に使用されますが、その使用法のほとんどがひどいということは、パターン自体をひどくしません。まれですが、シングルトンが最良のオプションである場合があります。
カイザールディ2015

47

ここにあなたは素晴らしい例を見ることができます。仮想メソッドを使用する場合、プログラムは実行時に何が実行されるかを認識します。CRTPを実装すると、コンパイラーがコンパイル時に決定します。これは素晴らしいパフォーマンスです!

template <class T>
class Writer
{
  public:
    Writer()  { }
    ~Writer()  { }

    void write(const char* str) const
    {
      static_cast<const T*>(this)->writeImpl(str); //here the magic is!!!
    }
};


class FileWriter : public Writer<FileWriter>
{
  public:
    FileWriter(FILE* aFile) { mFile = aFile; }
    ~FileWriter() { fclose(mFile); }

    //here comes the implementation of the write method on the subclass
    void writeImpl(const char* str) const
    {
       fprintf(mFile, "%s\n", str);
    }

  private:
    FILE* mFile;
};


class ConsoleWriter : public Writer<ConsoleWriter>
{
  public:
    ConsoleWriter() { }
    ~ConsoleWriter() { }

    void writeImpl(const char* str) const
    {
      printf("%s\n", str);
    }
};

定義してこれを行うことはできませんvirtual void write(const char* str) const = 0;か?公平であるとはいえ、このテクニックwriteは他の作業をしているときに非常に役立ちます。
atlex2

24
純粋な仮想メソッドを使用して、コンパイル時ではなくランタイムで継承を解決します。CRTPはコンパイル時にこれを解決するために使用されるため、実行はより高速になります。
GutiMac 2016

1
抽象Writerを必要とするプレーンな関数を作成してみてください。Writerという名前のクラスがどこにもないため、それを行うことはできません。これは仮想関数とまったく同じではなく、あまり役に立ちません。

22

CRTPは、コンパイル時のポリモーフィズムを実装する手法です。これは非常に単純な例です。以下の例でProcessFoo()は、はBaseクラスインターフェイスを操作しBase::Foo、派生オブジェクトのfoo()メソッドを呼び出します。これは、仮想メソッドで行うことを目的としています。

http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e

template <typename T>
struct Base {
  void foo() {
    (static_cast<T*>(this))->foo();
  }
};

struct Derived : public Base<Derived> {
  void foo() {
    cout << "derived foo" << endl;
  }
};

struct AnotherDerived : public Base<AnotherDerived> {
  void foo() {
    cout << "AnotherDerived foo" << endl;
  }
};

template<typename T>
void ProcessFoo(Base<T>* b) {
  b->foo();
}


int main()
{
    Derived d1;
    AnotherDerived d2;
    ProcessFoo(&d1);
    ProcessFoo(&d2);
    return 0;
}

出力:

derived foo
AnotherDerived foo

1
この例では、Derivedが実装していない場合に呼び出されるデフォルトのfoo()をBaseクラスに実装する方法の例を追加することも価値があります。別名ベースのfooを他の名前(caller()など)に変更し、新しい関数foo()をベースに追加して、その「ベース」を追加します。次に、
ProcessFoo

@wizurdこの例は、純粋な仮想基本クラス関数を示すためfoo()のものです。つまり、派生クラスによって実装される機能を強制します。
blueskin

3
これは、このパターンがProcessFoo()関数で役立つ理由も示しているため、私のお気に入りの答えです。
Pietro

このコードの要点はvoid ProcessFoo(T* b)わかりません。DerivedとAnotherDerivedが実際に派生していてもいなくても、引き続き機能するからです。私見ProcessFooが何らかの形でテンプレートを使用しなかった場合は、さらに興味深いでしょう。
Gabriel Devillers

1
@GabrielDevillersまず、テンプレート化ProcessFoo()されたものは、インターフェイスを実装する任意のタイプで機能します。つまり、この場合、入力タイプTにはと呼ばれるメソッドが必要foo()です。第2に、テンプレート化さProcessFooれていないものを複数のタイプで機能させるために、RTTIを使用することになりますが、これは避けたいものです。さらに、テンプレート化されたバージョンは、インターフェースでコンパイル時間チェックを提供します。
ブルースキン

6

これは直接的な回答ではなく、CRTPがどのように役立つかを示す例です。


良い具体例CRTPはあるstd::enable_shared_from_thisC ++ 11から。

[util.smartptr.enab] / 1

クラスTはを継承して、enable_­shared_­from_­this<T>を指すインスタンスshared_­from_­thisを取得するメンバー関数を継承できshared_­ptrます*this

つまり、から継承std::enable_shared_from_thisすると、インスタンスへのアクセスなしでインスタンスへの共有(または弱い)ポインタを取得できます(例:知っているだけのメンバー関数から)*this

を与える必要があるstd::shared_ptrが、アクセスできるのは*this次の場合のみです:

struct Node;

void process_node(const std::shared_ptr<Node> &);

struct Node : std::enable_shared_from_this<Node> // CRTP
{
    std::weak_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;

    void add_child(std::shared_ptr<Node> child)
    {
        process_node(shared_from_this()); // Shouldn't pass `this` directly.
        child->parent = weak_from_this(); // Ditto.
        children.push_back(std::move(child));
    }
};

this代わりに直接渡すことができない理由shared_from_this()は、所有権メカニズムを壊すためです。

struct S
{
    std::shared_ptr<S> get_shared() const { return std::shared_ptr<S>(this); }
};

// Both shared_ptr think they're the only owner of S.
// This invokes UB (double-free).
std::shared_ptr<S> s1 = std::make_shared<S>();
std::shared_ptr<S> s2 = s1->get_shared();
assert(s2.use_count() == 1);

5

ちょうど注意してください:

CRTPは、静的なポリモーフィズム(動的なポリモーフィズムに似ていますが、仮想関数ポインターテーブルがない)を実装するために使用できます。

#pragma once
#include <iostream>
template <typename T>
class Base
{
    public:
        void method() {
            static_cast<T*>(this)->method();
        }
};

class Derived1 : public Base<Derived1>
{
    public:
        void method() {
            std::cout << "Derived1 method" << std::endl;
        }
};


class Derived2 : public Base<Derived2>
{
    public:
        void method() {
            std::cout << "Derived2 method" << std::endl;
        }
};


#include "crtp.h"
int main()
{
    Derived1 d1;
    Derived2 d2;
    d1.method();
    d2.method();
    return 0;
}

出力は次のようになります:

Derived1 method
Derived2 method

1
すみません、static_castが変更を処理します。エラーが発生しなくてもコーナーケースを表示するには、ideone.com
odinthenerd

30
悪い例。このコードはvtableCRTPを使用せずにsなしで実行できます。がvtable実際に提供するのは、基本クラス(ポインターまたは参照)を使用して派生メソッドを呼び出すことです。ここで、CRTPでどのように行われるかを示す必要があります。
Etherealone

17
あなたの例でBase<>::method ()は、は呼び出されていませんし、どこでもポリモーフィズムを使用していません。
MikeMB 2015年

1
@Jichao、@MikeMBのノートによると、あなたが呼び出す必要がありmethodImplmethodBase派生クラスでは、名前methodImplの代わりにmethod
イワンクシュ

1
同様のmethod()を使用すると、静的にバインドされ、共通の基本クラスは必要ありません。とにかく、基本クラスのポインターまたは参照を介してそれを多態的に使用することができなかったからです。したがって、コードは次のようになります。#include <iostream> template <typename T> struct Writer {void write(){static_cast <T *>(this)-> writeImpl(); }}; struct Derived1:public Writer <Derived1> {void writeImpl(){std :: cout << "D1"; }}; struct Derived2:public Writer <Derived2> {void writeImpl(){std :: cout << "DER2"; }};
barney
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.