Дивно рекурсивний шаблон
Дивно рекурсивний шаблон (англ. 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].
Примітки
- Coplien, James O. (1995, February). Curiously Recurring Template Patterns. C++ Report: 24–27.
- Alexandrescu, Andrei (2001). Сучасне проєктування на C++: Generic Programming and Design Patterns Applied. Addison-Wesley. ISBN 0-201-70431-5.
- Simulated Dynamic Binding. 7 травня 2003. Архів оригіналу за 20 липня 2013. Процитовано 13 січня 2012.
- Arena, Marco. Use CRTP for polymorphic chaining. Процитовано 15-03-2017.
- Rainer Grimm (13 лютого 2017). C++ is Lazy: CRTP.