Menu
CoddyTech

Миксины через 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, чтобы составлять нужную функциональность, — это гибкая альтернатива полиморфизму во время выполнения, когда типы известны во время компиляции.

challenge icon

Задание

Легко

Давайте создадим систему журналирования, которая использует примеси 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. Здоровье врага (целое число)

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

    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 получают одинаковые возможности, наследуя одни и те же шаблоны примесей, но у каждого класса свой отдельный счётчик экземпляров, поскольку шаблон инстанцируется с разными типами.

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

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

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор C++