CRTP в C++

b147c06aba5e1b061aa22cc3d72b8fe3.png

Привет, Хабр!

CRTP — это метод в C++, при котором класс наследуется от шаблона класса, используя себя в качестве параметра шаблона. Это выглядит примерно так: класс X наследуется от класса-шаблона Y. Этот паттерн позволяет базовому классу напрямую обращаться к методам производного класса. С помощью CRTP можно можно обогатить интерфейс производного класса, внеся в него дополнительные методы через базовый класс-шаблон.

С CRTP также можно достигнуть полиморфизма во время компиляции и таким образом избваиться от затрат на производительность, связанных с динамическим полиморфизмом и виртуальными функциями.

Как реализовать в C++

Обычно создание CRTP начинается с определения базового класса как шаблона, который принимает тип производного класса в качестве параметра шаблона. Это позволяет базовому классу обращаться к членам и методам производного класса.

Производный класс должен наследоваться от базового класса-шаблона, передавая себя в качестве аргумента шаблона. Это создает рекурсивную зависимость, где базовый класс может использовать функциональность производного класса.

Далее в базовом классе можно юзать static_cast для приведения указателя this к типу производного класса, что позволяет вызывать его методы напрямую.

Пример: счетчик объектов

Создадим CRTP на классе для счетчика объектов:

Начнем с определения базового класса Counter, который будет учитывать количество созданных и еще существующих объектов производных классов. Базовый класс будет использовать CRTP, принимая производный класс в качестве параметра шаблона:

template
class Counter {
public:
    Counter() {
        ++created;
        ++alive;
    }

    Counter(const Counter&) {
        ++created;
        ++alive;
    }

    ~Counter() {
        --alive;
    }

    static int howManyAlive() {
        return alive;
    }

    static int howManyCreated() {
        return created;
    }

private:
    static int created;
    static int alive;
};

template
int Counter::created(0);

template
int Counter::alive(0);

Теперь можно создать производные классы, которые будут наследоваться от Counter, передавая себя в качестве параметра шаблона. Это позволит каждому производному классу иметь свои счетчики созданных и существующих объектов:

class MyClass : public Counter {
    // класс с каким-то функционалом
};

class AnotherClass : public Counter {
    // еще один класс с другим функционалом
};

Можно создать объекты этих классов и посмотреть, как работает счетчик, определенный в базовом классе Counter:

int main() {
    MyClass a, b;
    AnotherClass c;

    std::cout << "MyClass alive: " << MyClass::howManyAlive() << std::endl;
    std::cout << "AnotherClass alive: " << AnotherClass::howManyAlive() << std::endl;

    {
        MyClass d;
        std::cout << "MyClass alive (in scope): " << MyClass::howManyAlive() << std::endl;
    }

    std::cout << "MyClass alive (out of scope): " << MyClass::howManyAlive() << std::endl;
    std::cout << "MyClass created: " << MyClass::howManyCreated() << std::endl;
    std::cout << "AnotherClass alive: " << AnotherClass::howManyAlive() << std::endl;
    std::cout << "AnotherClass created: " << AnotherClass::howManyCreated() << std::endl;

    return 0;
}

Пример: статистический полиморфизм

Определим базовый класс Vehicle, который будет использоваться для реализации статического полиморфизма через CRTP.Класс будет содержать метод getNumberOfWheels, который делегирует вызов производному классу:

template
class Vehicle {
public:
    int getNumberOfWheels() const {
        // статическое приведение к производному типу и вызов его реализации
        return static_cast(this)->getNumberOfWheelsImpl();
    }
};

Определим несколько производных классов, таких как Car и Bicycle, каждый из которых наследуется от Vehicle, передавая себя в качестве параметра шаблона. Эти классы будут предоставлять собственную реализацию метода getNumberOfWheelsImpl:

class Car : public Vehicle {
public:
    int getNumberOfWheelsImpl() const {
        return 4; // автомобили обычно имеют 4 колеса
    }
};

class Bicycle : public Vehicle {
public:
    int getNumberOfWheelsImpl() const {
        return 2; // у велосипедов обычно 2 колеса
    }
};

Теперь можно создать объекты транспортных средств и запросить количество колес, используя единственный интерфейс, предоставляемый базовым классом Vehicle.

int main() {
    Car myCar;
    Bicycle myBicycle;

    std::cout << "Car has " << myCar.getNumberOfWheels() << " wheels.\n";
    std::cout << "Bicycle has " << myBicycle.getNumberOfWheels() << " wheels.\n";

    return 0;
}

Частые ошибки

Скрытие методов

Когда производный класс переопределяет метод, объявленный в базовом классе CRTP, возможно скрытие метода базового класса. Так вызовы методов базового класса будут не доступны для объектов производного класса, даже если такой вызов предполагался.

Чтобы избежать этой проблемы, нужно адекватно планировать иерархию наследования и именование методов, чтобы предотвратить нежелательное скрытие методов. Также можно использовать квалифицированные вызовы методов через this->method() для явного указания на методы базового класса.

Неопределенное поведение при неправильном наследовании

Если производный класс неправильно использует CRTP, т.е наследуется от Base вместо Base, это может привести к неопределенному поведению.

Чтобы предотвратить такие ошибки, можно добавить в базовый класс конструктор по умолчанию в закрытой секции и сделать производный класс дружественным.

Применяйте CRTP только там, где это действительно оправдано! CRTP идеально подходит для создания статического полиморфизма, но может усложнить код, если использовать его без необходимости.

В завершение хочу порекомендовать вам бесплатный урок курса C++ Developer. Professional про контейнеры STL. Регистрация доступна по ссылке.

© Habrahabr.ru