Миксины через CRTP
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy. Урок 77 из 104.
Любопытно рекурсивно повторяющийся шаблон (CRTP) — это техника, при которой класс наследуется от базового класса-шаблона, передавая себя в качестве аргумента шаблона. Это обеспечивает полиморфизм во время компиляции и позволяет базовым классам обращаться к членам производного класса без виртуальных функций.
Базовая структура CRTP выглядит так:
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class MyClass : public Base<MyClass> {
public:
void implementation() {
std::cout << "MyClass implementation\n";
}
};CRTP особенно эффективно подходит для создания примесей — повторно используемой функциональности, которую можно «подмешивать» в классы. В отличие от традиционного наследования, примеси добавляют возможности, не создавая глубоких иерархий:
template <typename Derived>
class Printable {
public:
void print() const {
const Derived& self = static_cast<const Derived&>(*this);
std::cout << self.toString() << "\n";
}
};
class Person : public Printable<Person> {
std::string name;
public:
Person(const std::string& n) : name(n) {}
std::string toString() const { return "Person: " + name; }
};
// Использование:
Person p("Alice");
p.print(); // Вывод: Person: AliceКлючевое преимущество заключается в том, что все вызовы методов разрешаются во время компиляции, что устраняет накладные расходы на виртуальные функции. Можно комбинировать несколько примесей CRTP, чтобы составлять нужную функциональность, — это гибкая альтернатива полиморфизму во время выполнения, когда типы известны во время компиляции.
Задание
ЛегкоДавайте создадим систему журналирования, которая использует примеси CRTP для добавления повторно используемой функциональности в разные классы без накладных расходов на виртуальные функции. Вы создадите две примеси, которые можно «подмешать» в любой класс: одну для подсчёта экземпляров, а другую для создания строковых представлений.
Вы организуете код в трёх файлах:
Mixins.h: Определите два шаблона примесей CRTP, предоставляющих повторно используемую функциональность.Создайте шаблон примеси
Countable, который отслеживает количество существующих экземпляров производного класса. В нём должен быть статический счётчик, который увеличивается в конструкторе и уменьшается в деструкторе. Добавьте статический методgetCount(), возвращающий текущее значение счётчика.Создайте шаблон примеси
Describable, предоставляющий методdescribe(). Этот метод должен использоватьstatic_castдля доступа к производному классу и вызова его методаgetDescription(), а затем выводить результат с переводом строки.Помните, что примеси CRTP используют
static_cast<Derived*>(this)для доступа к членам производного класса во время компиляции.Entities.h: Определите два класса сущностей, наследующих обе примеси.Создайте класс
Player, наследующийCountable<Player>иDescribable<Player>. Он должен хранить имя и уровень, а также предоставлять методgetDescription(), возвращающий строку в формате:Player: [name] (Level [level])Создайте класс
Enemy, который также наследует обе примеси. Он должен хранить тип и здоровье, а также предоставлять методgetDescription(), возвращающий:Enemy: [type] with [health] HPНе забудьте инициализировать статический счётчик для каждого класса.
main.cpp: Считайте четыре входных значения (каждое на отдельной строке):- Имя игрока (строка)
- Уровень игрока (целое число)
- Тип врага (строка)
- Здоровье врага (целое число)
Создайте игрока и врага с указанными значениями. Затем продемонстрируйте работу примесей:
- Выведите
Player count: [count], используя статический методgetCount() - Выведите
Enemy count: [count] - Вызовите
describe()для игрока - Вызовите
describe()для врага - Создайте второго игрока с именем "Guest" и уровнем 1
- Снова выведите
Player count: [count], чтобы показать обновлённое количество - Вызовите
describe()для второго игрока
Например, для входных значений Hero, 10, Dragon и 500:
Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)Эта задача демонстрирует, как примеси CRTP добавляют функциональность (подсчёт и описание) к несвязанным классам без использования виртуальных функций. И Player, и Enemy получают одинаковые возможности, наследуя одни и те же шаблоны примесей, но у каждого класса свой отдельный счётчик экземпляров, поскольку шаблон инстанцируется с разными типами.
Попробуйте сами
#include <iostream>
#include <string>
#include "Entities.h"
using namespace std;
int main() {
// Считать входные данные
string playerName;
int playerLevel;
string enemyType;
int enemyHealth;
cin >> playerName;
cin >> playerLevel;
cin >> enemyType;
cin >> enemyHealth;
// TODO: Создать объект Player с заданным именем и уровнем
// TODO: Создать объект Enemy с заданным типом и здоровьем
// TODO: Вывести "Player count: [count]", используя Player::getCount()
// TODO: Вывести "Enemy count: [count]", используя Enemy::getCount()
// TODO: Вызвать describe() для игрока
// TODO: Вызвать describe() для врага
// TODO: Создать второго игрока с именем "Guest" и уровнем 1
// TODO: Снова вывести "Player count: [count]"
// TODO: Вызвать describe() для второго игрока
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнерПотренируйтесь самостоятельно: Онлайн-компилятор C++