Что такое необычно повторяющийся шаблонный шаблон (CRTP)?

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


person Alok Save    schedule 13.11.2010    source источник
comment
Прочтите вопросы CRTP по SO: stackoverflow.com/questions/tagged/crtp. Это может дать вам некоторое представление.   -  person sbi    schedule 13.11.2010
comment
@sbi: Если он это сделает, он найдет свой вопрос. И это будет любопытно повторяться. :)   -  person Craig McQueen    schedule 08.01.2013
comment
Кстати, мне кажется, что этот термин должен повторяться. Я неправильно понимаю смысл?   -  person Craig McQueen    schedule 08.01.2013
comment
Крейг: Думаю, да; это любопытно повторяется в том смысле, что было обнаружено, что оно возникает в разных контекстах.   -  person Gareth McCaughan    schedule 20.04.2016


Ответы (5)


Короче говоря, 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

person Armen Tsirunyan    schedule 13.11.2010
comment
@ Армен Цирунян: Хороший ответ! спасибо, это помогает :) Во втором примере, который вы предоставили, шаблонный класс может использоваться только как базовый класс для CRTP, поскольку предположение ==. В общем, в STL шаблонные классы реализованы и специально помечены для использования в CRTP или реализованы независимо, без каких-либо зависимостей / предположений, как во втором примере? - person Alok Save; 13.11.2010
comment
@Als: Нет, контейнеры STL не разрабатываются с учетом CRTP. Все их операторы предоставляются независимо. - person Armen Tsirunyan; 13.11.2010
comment
Другой пример из реального мира - объект CComBaseClass (?) От Microsoft. - person John Dibling; 13.11.2010
comment
Этот пост не защищает синглтон как хороший шаблон программирования. Он просто использует его как иллюстрацию, которую можно понять. - person John Dibling; 13.11.2010
comment
@Armen: Ответ объясняет CRTP таким образом, чтобы его можно было четко понять, это хороший ответ, спасибо за такой хороший ответ. - person Alok Save; 16.11.2010
comment
@Armen: спасибо за отличное объяснение. Раньше я как бы не получал CRTP, но пример с равенством меня проясняет! +1 - person Paul; 08.04.2011
comment
Еще один пример использования CRTP - это когда вам нужен не копируемый класс: template ‹class T› class NonCopyable {protected: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); NonCopyable & operator = (const NonCopyable &); }; Затем вы используете noncopyable, как показано ниже: class Mutex: private NonCopyable ‹Mutex› {public: void Lock () {} void UnLock () {}}; - person Viren; 21.04.2014
comment
Мне нравится твой пример. Это очень хорошо показывает, на что способен CRTP. К сожалению, написанный вами код на самом деле не создаст настоящего синглтона, поскольку общедоступный конструктор класса A по-прежнему доступен. Поэтому пользователи по-прежнему могут создавать несколько экземпляров A. Если вы хотите изменить это на истинный синглтон, добавьте частный конструктор в A, переместите конструкторы синглтона в защищенный, добавьте статический фабричный метод в A. Move p = new ActualClass; к статическому фабричному методу в A и вызовите фабричный метод изнутри singleton :: getinstance (). Это настоящая боль, но это работает. - person Zachary Kraus; 27.08.2014
comment
@Puppy: Синглтон не страшен. Программисты уровня ниже среднего им злоупотребляют, когда другие подходы были бы более подходящими, но то, что большинство его применений ужасно, не делает сам шаблон ужасным. Бывают случаи, когда синглтон является лучшим вариантом, хотя это случается редко. - person Kaiserludi; 28.07.2015
comment
Прохладный. Предположим теперь, что я хочу наделить class A несколькими свойствами, например, я хочу, чтобы A был одноэлементным и имел operator==. Разве предложенная стратегия не приведет меня к множественному наследованию со всеми вытекающими отсюда оговорками? - person Michael; 05.01.2017
comment
Я думаю, что терминология специализация здесь должна быть заменена на создание экземпляра. Добро пожаловать, чтобы увидеть мой ответ о разнице между этими двумя концепциями. - person Francis; 23.01.2018
comment
Что произойдет, если два класса будут производными от одной и той же базовой специализации шаблона? - person Maestro; 17.12.2018
comment
Пример синглтона хорош. Но есть ошибка компиляции. Измените доступ Singleton() и других 3 функций на protected и измените доступ ActualClass* p на private. - person where23; 10.07.2019
comment
можем ли мы рассматривать CRTP как скомпилированный миксин, предоставляемый через определенный интерфейс? - person VladiC4T; 05.06.2021
comment
@ VladiC4T один из способов реализации миксина на C ++ - сделать его CRT, да - person Caleth; 23.07.2021

Вот отличный пример. Если вы используете виртуальный метод, программа будет знать, что выполняется во время выполнения. Реализуя 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);
    }
};
person GutiMac    schedule 03.11.2014
comment
Не могли бы вы сделать это, определив virtual void write(const char* str) const = 0;? Хотя, честно говоря, этот метод кажется очень полезным, когда write выполняет другую работу. - person atlex2; 09.08.2016
comment
Используя чистый виртуальный метод, вы решаете наследование во время выполнения, а не во время компиляции. CRTP используется для решения этой проблемы во время компиляции, поэтому выполнение будет быстрее. - person GutiMac; 10.08.2016
comment
Попробуйте создать простую функцию, которая ожидает абстрактный Writer: вы не можете этого сделать, потому что нигде нет класса с именем Writer, так где именно ваш полиморфизм? Это совсем не эквивалентно виртуальным функциям и гораздо менее полезно. - person ; 11.02.2019

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
person blueskin    schedule 09.03.2018
comment
В этот пример также может быть полезно добавить пример того, как реализовать foo () по умолчанию в базовом классе, который будет вызываться, если ни один из Derived не реализовал его. AKA измените foo в Base на другое имя (например, caller ()), добавьте новую функцию foo () в Base, которая cout's Base. Затем вызовите caller () внутри ProcessFoo - person wizurd; 11.04.2018
comment
@wizurd Этот пример больше иллюстрирует функцию чистого виртуального базового класса, т.е. мы обеспечиваем реализацию foo() производным классом. - person blueskin; 04.06.2018
comment
Это мой любимый ответ, поскольку он также показывает, почему этот шаблон полезен с функцией ProcessFoo(). - person Pietro; 11.09.2018
comment
Я не понимаю сути этого кода, потому что с void ProcessFoo(T* b) и без фактического извлечения Derived и AnotherDerived он все равно будет работать. ИМХО было бы интереснее, если бы ProcessFoo как-то не использовал шаблоны. - person Gabriel Devillers; 04.06.2020
comment
@GabrielDevillers Во-первых, шаблонный ProcessFoo() будет работать с любым типом, реализующим интерфейс, т.е. в этом случае тип ввода T должен иметь метод с именем foo(). Во-вторых, чтобы заставить нешаблонный ProcessFoo работать с несколькими типами, вы, скорее всего, в конечном итоге будете использовать RTTI, чего мы хотим избежать. Кроме того, в шаблонной версии вы можете проверить время компиляции в интерфейсе. - person blueskin; 12.06.2020

Это не прямой ответ, а скорее пример того, как 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);
person Mário Feroldi    schedule 27.11.2017

Как примечание:

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
person Jichao    schedule 11.10.2013
comment
Я думаю, что это сломается, как только вы используете Base с производным, который также является полиморфным во время выполнения, потому что статическое приведение указателя к базе не приведет к правильному указателю на производный. - person odinthenerd; 06.12.2013
comment
@PorkyBrain: Я не могу понять ... не могли бы вы привести реальный пример? - person Jichao; 06.12.2013
comment
извините, моя ошибка, static_cast позаботится об изменении. Если вы все равно хотите увидеть угловой корпус, даже если он не вызывает ошибки, см. Здесь: ideone.com/LPkktf - person odinthenerd; 06.12.2013
comment
понял, static_cast переместил объект вверх. - person Jichao; 06.12.2013
comment
Плохой пример. Этот код может быть выполнен без vtable без использования CRTP. Что действительно предоставляет vtables, так это использование базового класса (указателя или ссылки) для вызова производных методов. Здесь вы должны показать, как это делается с помощью CRTP. - person Etherealone; 06.02.2014
comment
В вашем примере Base<>::method () даже не вызывается, и вы нигде не используете полиморфизм. - person MikeMB; 12.03.2015
comment
@Jichao, согласно примечанию @MikeMB, вы должны вызывать methodImpl в method Base и в производных классах имя methodImpl вместо method - person Ivan Kush; 17.09.2016
comment
если вы используете аналогичный метод (), то он статически привязан, и вам не нужен общий базовый класс. Потому что в любом случае вы не можете использовать его полиморфно через указатель базового класса или ref. Таким образом, код должен выглядеть так: #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; }}; - person barney; 02.06.2017
comment
CRTP может использоваться для реализации статического полиморфизма, но статический полиморфизм не похож на динамический полиморфизм без vtables. Если бы это было так, компиляторы давно бы оптимизировали vtables. При использовании CRTP, если вы передаете производный объект функции, ожидающей Base, полиморфизм не будет работать, что противоречит цели. - person ; 11.02.2019