Menu
Coddy logo textTech

Миксины через CRTP

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 77 из 104.

Curiously Recurring Template Pattern (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 для компоновки функциональности, что делает этот подход гибкой альтернативой полиморфизму времени выполнения, когда типы известны на этапе компиляции.

challenge icon

Задание

Легко

Давайте создадим систему логирования, которая использует примеси (mixins) на основе 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: Считайте четыре входных значения (каждое на отдельной строке):
    1. Имя игрока (строка)
    2. Уровень игрока (целое число)
    3. Тип врага (строка)
    4. Здоровье врага (целое число)

    Создайте Player и Enemy с заданными значениями. Затем продемонстрируйте работу примесей:

    1. Выведите Player count: [count], используя статический метод getCount()
    2. Выведите Enemy count: [count]
    3. Вызовите describe() для игрока
    4. Вызовите describe() для врага
    5. Создайте второго игрока с именем "Guest" и уровнем 1
    6. Снова выведите Player count: [count], чтобы показать обновленный счетчик
    7. Вызовите 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 получают одинаковые возможности, наследуясь от одних и тех же шаблонов примесей, но каждый из них поддерживает свой собственный отдельный счетчик экземпляров, так как шаблон инстанцируется с разными типами.

Шпаргалка

Странно рекурсивный шаблон проектирования (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; }
};

Основные преимущества:

  • Все вызовы методов разрешаются во время компиляции
  • Отсутствие накладных расходов на виртуальные функции
  • Несколько миксинов могут быть объединены для компоновки функциональности
  • Каждый экземпляр шаблона сохраняет отдельное состояние (например, отдельные статические счетчики для каждого производного типа)

Попробуйте сами

#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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование