Привет, Хабр!
CRTP — это метод в C++, при котором класс наследуется от шаблона класса, используя себя в качестве параметра шаблона. Это выглядит примерно так: класс X
наследуется от класса-шаблона Y<X>
. Этот паттерн позволяет базовому классу напрямую обращаться к методам производного класса. С помощью CRTP можно можно обогатить интерфейс производного класса, внеся в него дополнительные методы через базовый класс-шаблон.
С CRTP также можно достигнуть полиморфизма во время компиляции и таким образом избваиться от затрат на производительность, связанных с динамическим полиморфизмом и виртуальными функциями.
Как реализовать в C++
Обычно создание CRTP начинается с определения базового класса как шаблона, который принимает тип производного класса в качестве параметра шаблона. Это позволяет базовому классу обращаться к членам и методам производного класса.
Производный класс должен наследоваться от базового класса-шаблона, передавая себя в качестве аргумента шаблона. Это создает рекурсивную зависимость, где базовый класс может использовать функциональность производного класса.
Далее в базовом классе можно юзать static_cast
для приведения указателя this
к типу производного класса, что позволяет вызывать его методы напрямую.
Пример: счетчик объектов
Создадим CRTP на классе для счетчика объектов:
Начнем с определения базового класса Counter
, который будет учитывать количество созданных и еще существующих объектов производных классов. Базовый класс будет использовать CRTP, принимая производный класс в качестве параметра шаблона:
template<typename T>
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<typename T>
int Counter<T>::created(0);
template<typename T>
int Counter<T>::alive(0);
Теперь можно создать производные классы, которые будут наследоваться от Counter
, передавая себя в качестве параметра шаблона. Это позволит каждому производному классу иметь свои счетчики созданных и существующих объектов:
class MyClass : public Counter<MyClass> {
// класс с каким-то функционалом
};
class AnotherClass : public Counter<AnotherClass> {
// еще один класс с другим функционалом
};
Можно создать объекты этих классов и посмотреть, как работает счетчик, определенный в базовом классе 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<typename Derived>
class Vehicle {
public:
int getNumberOfWheels() const {
// статическое приведение к производному типу и вызов его реализации
return static_cast<const Derived*>(this)->getNumberOfWheelsImpl();
}
};
Определим несколько производных классов, таких как Car
и Bicycle
, каждый из которых наследуется от Vehicle
, передавая себя в качестве параметра шаблона. Эти классы будут предоставлять собственную реализацию метода getNumberOfWheelsImpl
:
class Car : public Vehicle<Car> {
public:
int getNumberOfWheelsImpl() const {
return 4; // автомобили обычно имеют 4 колеса
}
};
class Bicycle : public Vehicle<Bicycle> {
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<AnotherClass>
вместо Base<DerivedClass>
, это может привести к неопределенному поведению.
Чтобы предотвратить такие ошибки, можно добавить в базовый класс конструктор по умолчанию в закрытой секции и сделать производный класс дружественным.
Применяйте CRTP только там, где это действительно оправдано! CRTP идеально подходит для создания статического полиморфизма, но может усложнить код, если использовать его без необходимости.
В завершение хочу порекомендовать вам бесплатный урок курса C++ Developer. Professional про контейнеры STL. Регистрация доступна по ссылке.
Комментарии (11)
Naf2000
04.04.2024 06:31+2Стоит упомянуть про нововведение c++23 Explicit object member functions
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html
MaxxONE
Смысл примера не понятен. В чем преимущество перед обычным наследованием?
код
ZirakZigil
В статическом полиморфизме.
nickolaym
Преимущества начинаются, когда у наследников есть полиморфные
статические члены: обычными виртуальными функциями их уже не пробросить, нужен механизм словарей/фабрик (в шарпе и яве это сделано на дженериках - ну или ручками, ручками всё)
свойства времени компиляции: типы, константы и всё такое - тут уже даже дженерики не спасут
ZirakZigil
Можно в копилку докинуть возможность запилить частичный интерфейс: базовый класс может детектировать наличие опциональных функций у наследников и вызывать их только если они есть. И всё это проверится статически
Прямо вот так оно работать не будет,
Impl
-то неполный в этот момент.AnimeSlave
Обычное наследование использует механизм виртуальных функций, который отнимает некоторое количество циклов процессора при работе, так как программа тратит некоторое время на «поиск» виртуальной функции перед её вызовом. Статический полиморфизм через CRTP избавляет от прослойки в виде виртуальных функций. Следствием является некоторое ускорение выполнение программы при определённых условиях. То есть, если у вас нет задачи выжимать наносекунды при выполнении вашей программы, то CRTP для конкретно вашей задачи не будет иметь преимуществ