Який цікаво повторюваний шаблон шаблону (CRTP)?


187

Не звертаючись до книги, чи може хто-небудь, будь ласка, надати хороше пояснення для CRTPприкладу коду?


2
Прочитайте питання CRTP на SO: stackoverflow.com/questions/tagged/crtp . Це може дати вам деяке уявлення.
sbi

68
@sbi: Якщо він це зробить, він знайде власне питання. І це було б цікаво повторюватися. :)
Крейг МакКуін

1
До речі, мені здається, що термін повинен бути "цікаво повторюваним". Я не розумію значення?
Крейг МакКуін

1
Крейг: Я думаю, що ти є; це "цікаво повторюється" в тому сенсі, що було виявлено, що він з'являється в декількох контекстах.
Гарет Маккоган

Відповіді:


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. Наприклад, якщо ви хочете порахувати всі екземпляри, які наразі існують для вашого класу, але хочете інкапсулювати цю логіку в окремий шаблон (ідея для конкретного класу досить проста - мати статичну змінну, приріст в ctors, декремент у 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 просто використовує його в якості ілюстрації , які можуть бути зазвичай understood.imo в-1 є невиправданим
Джон Dibling

3
@Armen: Відповідь пояснює CRTP так, що її можна зрозуміти чітко, це приємна відповідь, дякую за таку гарну відповідь.
Алок Зберегти

1
@Armen: дякую за це чудове пояснення. Раніше я не отримував CRTP, але приклад рівності висвітлюється! +1
Павло

1
Ще один приклад використання CRTP - це коли вам потрібен клас, який не можна скопіювати: шаблон <клас T> клас NonCopyable {захищений: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); NonCopyable & operator = (const NonCopyable &); }; Тоді ви використовуєте такі, які не можна скопіювати, як показано нижче: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Вірен

2
@Puppy: Singleton не страшний. Це далеко не надто зловживають програмісти нижче середнього рівня, коли інші підходи були б більш доречними, але те, що більшість його звичаїв є жахливими, не робить сам візерунок жахливим. Бувають випадки, коли одиночний - найкращий варіант, хоча вони рідкісні.
Кайзерлуді

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()робота з 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
Також у цьому прикладі може бути варто додати приклад того, як реалізувати foo () за замовчуванням у класі Base, який буде викликаний, якщо жоден Derived не реалізував його. AKA змінить foo у Base на якесь інше ім'я (наприклад, caller ()), додайте нову функцію foo () до Base, що cout's "Base". Потім зателефонуйте Caller () всередині ProcessFoo
wizurd

@wizurd Цей приклад скоріше ілюструє функцію чистого віртуального базового класу, тобто ми примусово foo()реалізуємо похідний клас.
блюскін

3
Це моя улюблена відповідь, оскільки вона також показує, чому цей шаблон корисний для ProcessFoo()функції.
П'єтро

Я не розумію цього коду, тому що з void ProcessFoo(T* b)і без отримання похідних та AnotherDerived він все-таки буде працювати. IMHO було б цікавіше, якби ProcessFoo якось не використовував шаблони.
Габріель Дияволс

1
@GabrielDevillers По-перше, шаблонований ProcessFoo()буде працювати з будь-яким типом, який реалізує інтерфейс, тобто в цьому випадку тип вводу T повинен мати метод, який називається foo(). По-друге, для того, щоб отримати не шаблоновану ProcessFooроботу з декількома типами, ви, швидше за все, використовуєте RTTI, чого ми хочемо уникати. Крім того, шаблонована версія забезпечує перевірку часу компіляції в інтерфейсі.
blueskin

6

Це не пряма відповідь, а скоріше приклад того, як CRTP може бути корисним.


Хороший конкретний приклад CRTP - std::enable_shared_from_thisз C ++ 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/LPkktf
odinthenerd

30
Поганий приклад. Цей код можна зробити без vtables без використання CRTP. Що vtableсправді забезпечують, це використання базового класу (вказівник або посилання) для виклику похідних методів. Ви повинні показати, як це робиться з CRTP тут.
Etherealone

17
У вашому прикладі Base<>::method ()навіть не називається, і ви ніде не використовуєте поліморфізм.
MikeMB

1
@Jichao, згідно з приміткою @MikeMB «S, ви повинні зателефонувати methodImplв methodз Baseі в похідних класах ім'я methodImplзамістьmethod
Іван Кущ

1
якщо ви використовуєте подібний метод (), то його статично пов'язаний і вам не потрібен загальний базовий клас. Тому що в будь-якому випадку ви не могли б використовувати його поліморфно через вказівник базового класу чи ref. Отже, код повинен виглядати так: #include <iostream> шаблон <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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.