Дивно рекурсивний шаблон

Дивно рекурсивний шаблон (англ. curiously recurring template pattern (CRTP)) — ідіома в мові програмування C++, коли клас X є похідним від шаблону класу, інстанційованого із використанням самого X як шаблонного аргументу. Назва для цієї ідіоми була запропонована Джимом Копліном,[1], котрий розглянув її в одному з найперших шаблонних кодів на C++.

Загальна форма

ред.
template <typename T>
struct base
{
    // ...
};
struct derived : base<derived>
{
    // ...
};

Цей підхід можна використати для реалізації статичного поліморфізму, а також в деяких інших техніках метапрограмування як описано Андрієм Александреску в його книзі — Сучасне проєктування на C++.[2]

Статичний поліморфізм

ред.
#include <iostream>
using namespace std;

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

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

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

template<typename T>
void bar(Base<T>& base)
{
    base.foo();
}

int main()
{
    A a;
    B b;
    bar(a);
    bar(b); 
    return 0;
}

Цей підхід дає ефект подібний до використання віртуальних функцій, без ціни (і деякої гнучкості) динамічного поліморфізму. Це використання CRTP дехто називає «симулюванням динамічного зв'язування».[3] Цей підхід широко використовується в Windows бібліотеках ATL і WTL.

Лічильник об'єктів

ред.

Головна ціль лічильника об'єктів — отримання статистики створення й руйнування об'єктів даного класу. Це можна легко реалізувати із використанням CRTP:

template <typename T>
struct counter
{
    static int objects_created;
    static int objects_alive;

    counter()
    {
        ++objects_created;
        ++objects_alive;
    }
protected:
    ~counter()
    {
        --objects_alive;
    }
};
template <typename T> int counter<T>::objects_created( 0 );
template <typename T> int counter<T>::objects_alive( 0 );

class X : counter<X>
{
    // ...
};

class Y : counter<Y>
{
    // ...
};

Ланцюг методів

ред.

Для спрощення багаторазового виклику методів одного об'єкта в об'єктно-орієнтованих мовах програмування існує популярний прийом, відомий як англ. method chaining (ланцюг методів). Кожен з цих методів повертає об'єкт, дозволяючи тим самим виклик методів послідовно в одному виразі, без створення додаткових змінних для збереження тимчасових чи проміжних результатів.

Однак у випадках, коли цей прийом застосувати до об'єктів, що мають ієрархію, виникає ускладнення. Припустимо ми маємо наступний базовий клас:

class Printer
{
public:
      Printer(ostream& pstream) : m_stream(pstream) {}
 
      template <typename T>
      Printer& print(T&& t) { m_stream << t; return *this; }
 
      template <typename T>
      Printer& println(T&& t) { m_stream << t << endl; return *this; }
private:
      ostream& m_stream;
};

Його методи можуть бути легко викликані по ланцюгу:

Printer{myStream}.println("hello").println(500);

Однак, коли ми задекларуємо похідний клас:

class CoutPrinter : public Printer
{
public:
     CoutPrinter() : Printer(cout) {}
 
     CoutPrinter& SetConsoleColor(Color c) { ... return *this; }
};

то «втратимо» інформацію про клас, при спробі викликати метод базового класу:

CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!"); 
// помилка компіляції, бо ми тут маємо об'єкт 'Printer', а не 'CoutPrinter'

Це трапилось тому, що 'print' є функцією базового класу — 'Printer', вона повертає екземпляр класу 'Printer'.

Дивно рекурсивний шаблон може бути використаний щоб запобігти вказаній проблемі і реалізувати «Поліморфний ланцюг»[4]:

// Базовий клас
template <typename ConcretePrinter>
class Printer
{
public:
      Printer(ostream& pstream) : m_stream(pstream) {}
 
      template <typename T>
      ConcretePrinter& print(T&& t)
      {
          m_stream << t;
          return static_cast<ConcretePrinter&>(*this);
      }
 
      template <typename T>
      ConcretePrinter& println(T&& t)
      {
          m_stream << t << endl;
          return static_cast<ConcretePrinter&>(*this);
      }
private:
      ostream& m_stream;
};
 
// Похідний клас
class CoutPrinter : public Printer<CoutPrinter>
{
public:
     CoutPrinter() : Printer(cout) {}
 
     CoutPrinter& SetConsoleColor(Color c) { ... return *this; }
};
 
// використання
 CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!");

Домішки

ред.

Дивно рекурсивний шаблон також може бути використаний для реалізації домішок. Поширеним прикладом є клас std::enable_shared_from_this, породжені класи від якого здатні повертати std::shared_ptr на свої екземпляри.

Тобто, якщо клас MySharedClass породжений від public std::enable_shared_from_this то він матиме метод shared_from_this, який повертатиме std::shared_ptr на екземпляр[5].

Примітки

ред.
  1. Coplien, James O. (1995, February). Curiously Recurring Template Patterns. C++ Report: 24—27.
  2. Alexandrescu, Andrei (2001). Сучасне проєктування на C++: Generic Programming and Design Patterns Applied. Addison-Wesley. ISBN 0-201-70431-5.
  3. Simulated Dynamic Binding. 7 травня 2003. Архів оригіналу за 20 липня 2013. Процитовано 13 січня 2012.
  4. Arena, Marco. Use CRTP for polymorphic chaining. Архів оригіналу за 27 лютого 2018. Процитовано 15-03-2017.
  5. Rainer Grimm (13 лютого 2017). C++ is Lazy: CRTP. Архів оригіналу за 18 грудня 2019. Процитовано 2 січня 2020.

Див. також

ред.