흥미로운 반복 템플릿 패턴 (CRTP)은 무엇입니까?


187

책을 언급하지 않고 누구나 CRTP코드 예제 를 통해 좋은 설명을 제공 할 수 있습니까?


2
SO에 대한 CRTP 질문을 읽으십시오 : stackoverflow.com/questions/tagged/crtp . 그것은 당신에게 몇 가지 아이디어를 줄 수 있습니다.
sbi

68
@ sbi : 만약 그렇게한다면, 그는 자신의 질문을 찾을 것입니다. 그리고 그것은 흥미롭게 반복 될 것입니다. :)
Craig McQueen

1
BTW,이 용어는 "신기하게 재귀"해야합니다. 나는 그 의미를 오해하고 있는가?
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 단위로 증가, dtors 감소) ). 운동으로 해보십시오!

Boost에 대한 또 다른 유용한 예 (그들이 어떻게 구현했는지 확실하지 않지만 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
좋은 프로그래밍을 pattern.it 단순히-1 understood.imo 일반적으로 할 수있는 그림으로 사용하기 때문에이 포스팅은 부당한입니다 싱글을 옹호하지 않습니다
존 Dibling

3
@Armen : 대답은 CRTP를 명확하게 이해할 수있는 방식으로 설명합니다. 좋은 대답입니다.
Alok 저장

1
@Armen :이 훌륭한 설명에 감사드립니다. 나는 이전에 CRTP를 얻지 못했지만 평등 예가 밝아졌습니다! +1
Paul

1
CRTP를 사용하는 또 다른 예는 복사 할 수없는 클래스가 필요한 경우입니다. template <class T> class NonCopyable {protected : NonCopyable () {} ~ NonCopyable () {} private : NonCopyable (const NonCopyable &); 비 복사 가능 및 연산자 = (const NonCopyable &); }; 그런 다음 아래와 같이 복사 할 수없는 것을 사용합니다. class Mutex : private NonCopyable <Mutex> {public : void Lock () {} void UnLock () {}};
Viren

2
@ 강아지 : 싱글 톤은 끔찍하지 않습니다. 다른 접근 방식이 더 적합 할 때 평균 이하의 프로그래머가 많이 사용하지만 대부분의 사용법이 끔찍한 것은 패턴 자체를 끔찍하게 만들지는 않습니다. 드물지만 싱글 톤이 최선의 선택 인 경우가 있습니다.
Kaiserludi

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

26
순수한 가상 방법을 사용하면 컴파일 시간 대신 런타임에서 상속을 해결할 수 있습니다. CRTP는 컴파일 타임에이를 해결하는 데 사용되므로 실행 속도가 빨라집니다.
GutiMac

1
추상 Writer가 필요한 일반 함수를 만들어보십시오. 어디에 Writer라는 클래스가 없으므로 다형성이 정확히 어디에 있습니까? 이것은 가상 함수와 전혀 같지 않으며 훨씬 덜 유용합니다.

22

CRTP는 컴파일 타임 다형성을 구현하는 기술입니다. 다음은 매우 간단한 예입니다. 아래 예제에서 클래스 인터페이스 ProcessFoo()로 작업 하고 파생 객체의 메소드를 호출합니다 . 이는 가상 메소드로 수행하려는 것입니다.BaseBase::Foofoo()

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
이 예제에서는 파생 클래스가 구현하지 않은 경우 호출되는 기본 클래스에서 기본 foo ()를 구현하는 방법에 대한 예제를 추가하는 것이 좋습니다. AKA Base의 foo를 다른 이름 (예 : caller ())으로 변경하고 새로운 함수 foo ()를 cout의 "Base"인 Base에 추가합니다. 그런 다음 ProcessFoo 내부에서 caller ()를 호출하십시오
wizurd

@wizurd이 예제는 순수한 가상 기본 클래스 함수를 설명하기위한 것 foo()입니다. 즉 우리 는 파생 클래스에 의해 구현됩니다.
blueskin

3
이 패턴이 ProcessFoo()함수에 유용한 이유를 보여주기 때문에 이것이 내가 가장 좋아하는 대답 입니다.
Pietro

void ProcessFoo(T* b)Derived와 AnotherDerived가 실제로 파생되어 있거나 그렇지 않은 경우에도 여전히 작동 하기 때문에이 코드의 요점을 알 수 없습니다 . IMHO ProcessFoo가 어떻게 든 템플릿을 사용하지 않으면 더 흥미로울 것입니다.
Gabriel Devillers

1
@GabrielDevillers 첫째, templatized ProcessFoo()는 인터페이스를 구현하는 모든 유형에서 작동합니다.이 경우 입력 유형 T에는라는 메소드가 있어야합니다 foo(). 둘째, 템플릿이 아닌 ProcessFoo여러 유형으로 작업하려면 RTTI를 사용하는 것이 좋습니다. 또한 템플릿 버전은 인터페이스에서 컴파일 시간을 확인합니다.
blueskin

6

이것은 직접적인 대답이 아니라 CRTP 가 유용한 방법의 예입니다 .


의 좋은 콘크리트 예를 CRTP는 이다 std::enable_shared_from_thisC ++ 11 :

[util.smartptr.enab] / 1

클래스 T는를 가리키는 인스턴스 를 얻는 멤버 함수 enable_­shared_­from_­this<T>를 상속하기 위해 상속 할 수 있습니다 .shared_­from_­thisshared_­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/LPkktf
odinthenerd

30
나쁜 예입니다. 이 코드는 vtableCRTP를 사용하지 않고 s없이 수행 할 수 있습니다 . 무엇을 vtable진정으로 제공하는 s의 것은 파생 된 메소드를 호출 할 기본 클래스 (포인터 또는 참조)를 사용합니다. 여기에서 CRTP로 어떻게 수행되는지 보여 주어야합니다.
Etherealone

17
귀하의 예 Base<>::method ()에서조차 불려지지 않으며 어디서나 다형성을 사용하지 않습니다.
MikeMB

1
@Jikeo, @MikeMB의 메모 methodImpl에 따르면 methodof of Base및 파생 클래스 이름 methodImpl대신에 전화해야합니다.method
Ivan Kush

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.