Любопытно повторяющийся шаблон шаблона
Любопытно повторяющийся шаблон шаблона ( CRTP ) — это идиома, первоначально разработанная в C++ , в которой класс X
класса происходит от создания экземпляра шаблона с использованием X
себя как аргумент шаблона. [1] В более общем смысле он известен как F-ограниченный полиморфизм и является формой F -ограниченной квантификации .
История
[ редактировать ]Этот метод был формализован в 1989 году как « F -ограниченная количественная оценка». [2] Название «CRTP» было независимо придумано Джимом Коплиеном в 1995 году. [3] кто наблюдал это в одном из самых ранних C++ шаблонов кода а также в примерах кода, которые Тимоти Бадд создал на своем мультипарадигмальном языке Leda. [4] Иногда его называют «перевернутым наследованием». [5] [6] из-за того, что он позволяет расширять иерархию классов путем замены разных базовых классов.
Реализация Microsoft CRTP в библиотеке активных шаблонов (ATL) была независимо обнаружена, также в 1995 году, Яном Фалькиным, который случайно получил базовый класс из производного класса. Кристиан Бомонт впервые увидел код Яна и поначалу подумал, что он не сможет скомпилироваться доступным в то время компилятором Microsoft. После того, как выяснилось, что это действительно работает, Кристиан основал всю конструкцию ATL и библиотеки шаблонов Windows (WTL) на этой ошибке. [ нужна ссылка ]
Общая форма
[ редактировать ]// The Curiously Recurring Template Pattern (CRTP)
template <class T>
class Base
{
// methods within Base can use template to access members of Derived
};
class Derived : public Base<Derived>
{
// ...
};
Некоторыми вариантами использования этого шаблона являются статический полиморфизм и другие методы метапрограммирования, например, описанные Андреем Александреску в книге Modern C++ Design . [7]
Он также занимает видное место в реализации парадигмы данных, контекста и взаимодействия на C++ . [8]
Кроме того, CRTP используется стандартной библиотекой C++ для реализации std::enable_shared_from_this
функциональность. [9]
Статический полиморфизм
[ редактировать ]Обычно шаблон базового класса использует тот факт, что тела функций-членов (определения) не создаются в течение долгого времени после их объявления, и будет использовать члены производного класса в своих собственных функциях-членах посредством использования приведения ; например:
template <class T>
struct Base
{
void interface()
{
// ...
static_cast<T*>(this)->implementation();
// ...
}
static void static_func()
{
// ...
T::static_sub_func();
// ...
}
};
struct Derived : public Base<Derived>
{
void implementation();
static void static_sub_func();
};
В приведенном выше примере функция Base<Derived>::interface()
, хотя и было объявлено до существования struct Derived
известно компилятору (т.е. до Derived
объявлен), фактически не создается компилятором до тех пор, пока он не будет фактически вызван каким-либо более поздним кодом, который происходит после объявления Derived
(не показано в приведенном выше примере), так что во время создания экземпляра функции «интерфейс» объявление Derived::implementation()
известно.
Этот метод достигает эффекта, аналогичного использованию виртуальных функций , без затрат (и некоторой гибкости) динамического полиморфизма . Некоторые называют это конкретное использование CRTP «имитируемой динамической привязкой». [10] Этот шаблон широко используется в библиотеках Windows ATL и WTL .
Чтобы уточнить приведенный выше пример, рассмотрим базовый класс без виртуальных функций . Всякий раз, когда базовый класс вызывает другую функцию-член, он всегда вызывает свои собственные функции базового класса. Когда мы наследуем класс от этого базового класса, мы наследуем все переменные-члены и функции-члены, которые не были переопределены (без конструкторов или деструкторов). Если производный класс вызывает унаследованную функцию, которая затем вызывает другую функцию-член, то эта функция никогда не будет вызывать какие-либо производные или переопределенные функции-члены в производном классе.
Однако если функции-члены базового класса используют CRTP для всех вызовов функций-членов, переопределенные функции в производном классе будут выбраны во время компиляции. Это эффективно эмулирует систему вызова виртуальных функций во время компиляции без затрат на размер или накладные расходы на вызов функций ( структуры VTBL и поиск методов, механизм VTBL с множественным наследованием), но не позволяет сделать этот выбор во время выполнения.
Счетчик объектов
[ редактировать ]Основная цель счетчика объектов — получение статистики создания и уничтожения объектов для данного класса. [11] Это можно легко решить с помощью CRTP:
template <typename T>
struct counter
{
static inline int objects_created = 0;
static inline int objects_alive = 0;
counter()
{
++objects_created;
++objects_alive;
}
counter(const counter&)
{
++objects_created;
++objects_alive;
}
protected:
~counter() // objects should never be removed through pointers of this type
{
--objects_alive;
}
};
class X : counter<X>
{
// ...
};
class Y : counter<Y>
{
// ...
};
Каждый раз, когда объект класса X
создается, конструктор counter<X>
вызывается, увеличивая как созданный, так и активный счетчик. Каждый раз, когда объект класса X
уничтожается, количество живых уменьшается. Важно отметить, что counter<X>
и counter<Y>
это два отдельных класса, поэтому они будут вести отдельный подсчет X
песок Y
с. В этом примере CRTP такое различие классов является единственным использованием параметра шаблона ( T
в counter<T>
) и причину, по которой мы не можем использовать простой базовый класс без шаблонов.
Полиморфная цепочка
[ редактировать ]Цепочка методов , также известная как идиома именованных параметров, представляет собой общий синтаксис для вызова нескольких вызовов методов в объектно-ориентированных языках программирования. Каждый метод возвращает объект, что позволяет объединить вызовы в одну инструкцию без необходимости использования переменных для хранения промежуточных результатов.
Когда шаблон объекта именованного параметра применяется к иерархии объектов, все может пойти не так. Предположим, у нас есть такой базовый класс:
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;
}
};
мы «теряем» конкретный класс, как только вызываем функцию базы:
// v----- we have a 'Printer' here, not a 'CoutPrinter'
CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!"); // compile error
Это происходит потому, что «печать» является функцией базы — «Принтер» — и затем возвращает экземпляр «Принтер».
CRTP можно использовать, чтобы избежать такой проблемы и реализовать «полиморфную цепочку»: [12]
// Base class
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;
};
// Derived class
class CoutPrinter : public Printer<CoutPrinter>
{
public:
CoutPrinter() : Printer(cout) {}
CoutPrinter& SetConsoleColor(Color c)
{
// ...
return *this;
}
};
// usage
CoutPrinter().print("Hello ").SetConsoleColor(Color.red).println("Printer!");
Полиморфная копия
[ редактировать ]При использовании полиморфизма иногда необходимо создавать копии объектов по указателю базового класса. Обычно для этого используется идиома — добавление функции виртуального клонирования, которая определена в каждом производном классе. CRTP можно использовать, чтобы избежать дублирования этой функции или других подобных функций в каждом производном классе.
// Base class has a pure virtual function for cloning
class AbstractShape {
public:
virtual ~AbstractShape () = default;
virtual std::unique_ptr<AbstractShape> clone() const = 0;
};
// This CRTP class implements clone() for Derived
template <typename Derived>
class Shape : public AbstractShape {
public:
std::unique_ptr<AbstractShape> clone() const override {
return std::make_unique<Derived>(static_cast<Derived const&>(*this));
}
protected:
// We make clear Shape class needs to be inherited
Shape() = default;
Shape(const Shape&) = default;
Shape(Shape&&) = default;
};
// Every derived class inherits from CRTP class instead of abstract class
class Square : public Shape<Square>{};
class Circle : public Shape<Circle>{};
Это позволяет получать копии квадратов, кругов или любых других фигур путем shapePtr->clone()
.
Подводные камни
[ редактировать ]Одна из проблем статического полиморфизма заключается в том, что без использования общего базового класса, такого как AbstractShape
В приведенном выше примере производные классы не могут храниться однородно, то есть помещать разные типы, производные от одного и того же базового класса, в один и тот же контейнер. Например, контейнер, определенный как std::vector<Shape*>
не работает, потому что Shape
это не класс, а шаблон, требующий специализации. Контейнер, определенный как std::vector<Shape<Circle>*>
могу только хранить Circle
с, не Square
с. Это связано с тем, что каждый из классов, производных от базового класса CRTP, Shape
это уникальный тип. Распространенным решением этой проблемы является наследование от общего базового класса с помощью виртуального деструктора, такого как AbstractShape
приведенный выше пример, позволяющий создать std::vector<AbstractShape*>
.
Вывод этого
[ редактировать ]Использование CRTP можно упростить с помощью функции C++23 , выяснив это. [13] [14] Для функции signature_dish
вызвать производную функцию-член cook_signature_dish
, ChefBase
должен быть шаблонного типа и CafeChef
необходимо наследовать от ChefBase
, передав его тип в качестве параметра шаблона.
template <typename T>
struct ChefBase
{
void signature_dish()
{
static_cast<T*>(this)->cook_signature_dish();
}
};
struct CafeChef : ChefBase<CafeChef>
{
void cook_signature_dish() {}
};
Если используется явный параметр объекта, ChefBase
не требует шаблонизации и CafeChef
может быть получено из ChefBase
ясно. Поскольку self
параметр автоматически выводится как правильный производный тип, приведение не требуется.
struct ChefBase
{
template <typename Self>
void signature_dish(this Self&& self)
{
self.cook_signature_dish();
}
};
struct CafeChef : ChefBase
{
void cook_signature_dish() {}
};
См. также
[ редактировать ]Ссылки
[ редактировать ]- ^ Абрахамс, Дэвид ; Гуртовой, Алексей (январь 2005 г.). Метапрограммирование шаблонов C++: концепции, инструменты и методы из Boost и за его пределами . Аддисон-Уэсли. ISBN 0-321-22725-5 .
- ^ Уильям Кук; и др. (1989). «F-ограниченный полиморфизм для объектно-ориентированного программирования» (PDF) .
- ^ Коплин, Джеймс О. (февраль 1995 г.). «Любопытно повторяющиеся шаблоны шаблонов» (PDF) . Отчет по C++ : 24–27.
- ^ Бадд, Тимоти (1994). Мультипарадигмальное программирование в Leda . Аддисон-Уэсли. ISBN 0-201-82080-3 .
- ^ «Кафе отступников: ATL и перевернутое наследование» . 15 марта 2006 г. Архивировано из оригинала 15 марта 2006 г. Проверено 9 октября 2016 г.
{{cite web}}
: CS1 maint: bot: исходный статус URL неизвестен ( ссылка ) - ^ «ATL и перевернутое наследование» . 4 июня 2003 г. Архивировано из оригинала 4 июня 2003 г. Проверено 9 октября 2016 г.
{{cite web}}
: CS1 maint: bot: исходный статус URL неизвестен ( ссылка ) - ^ Александреску, Андрей (2001). Современный дизайн на C++: применение общих шаблонов программирования и проектирования . Аддисон-Уэсли. ISBN 0-201-70431-5 .
- ^ Коплин, Джеймс ; Бьёрнвиг, Гертруда (2010). Бережливая архитектура: для гибкой разработки программного обеспечения . Уайли. ISBN 978-0-470-68420-7 .
- ^ "std::enable_shared_from_this" . Проверено 22 ноября 2022 г.
- ^ «Имитация динамического связывания» . 7 мая 2003 года. Архивировано из оригинала 9 февраля 2012 года . Проверено 13 января 2012 г.
- ^ Мейерс, Скотт (апрель 1998 г.). «Подсчет объектов в C++» . Журнал пользователей C/C++ .
- ^ Арена, Марко (29 апреля 2012 г.). «Используйте CRTP для полиморфной цепочки» . Проверено 15 марта 2017 г.
- ^ Гашпер Ажман; Сай Брэнд; Бен Дин; Барри Ревзин (12 июля 2021 г.). «Вывод этого» .
- ^ «Явный параметр объекта» . Проверено 27 декабря 2023 г.