Что такое CRTP-примесь
CRTP-примесь добавляет поведение классу, заставляя его наследовать от шаблонного базового класса, получающего конкретный класс в качестве параметра типа:
1template <typename Derived>
2class Mixin {
3 // можно вызывать методы Derived через static_cast<Derived*>(this)
4};
5
6class MyClass : public Mixin<MyClass> {
7 // MyClass теперь имеет всё, что предоставляет Mixin
8};
Никакой виртуальной диспетчеризации. Никакой vtable. Примесь встраивается при компиляции.
Примесь 1: Loggable
Добавляет метод log(msg), предваряющий сообщение именем класса:
1template <typename Derived>
2class Loggable {
3public:
4 void log(std::string_view msg) const {
5 auto& self = static_cast<const Derived&>(*this);
6 printf("[%s] %.*s\n", self.className(),
7 static_cast<int>(msg.size()), msg.data());
8 }
9};
10
11class SensorTask : public Loggable<SensorTask> {
12public:
13 const char* className() const { return "SensorTask"; }
14
15 void run() {
16 log("started");
17 // ...
18 log("done");
19 }
20};
Если производный класс не предоставляет className(), компилятор выдаёт ошибку
в точке первого вызова log — никакого краша во время выполнения.
Примесь 2: Toggleable
Добавляет состояние включения/отключения:
1template <typename Derived>
2class Toggleable {
3 bool enabled_ = true;
4public:
5 void enable() { enabled_ = true; }
6 void disable() { enabled_ = false; }
7 bool isEnabled() const { return enabled_; }
8
9protected:
10 // Вызывайте из методов производного класса, которые должны быть холостыми при отключении
11 bool checkEnabled(std::string_view op = "") const {
12 if (!enabled_ && !op.empty())
13 printf("[%s] disabled, ignoring %.*s\n",
14 static_cast<const Derived*>(this)->className(),
15 static_cast<int>(op.size()), op.data());
16 return enabled_;
17 }
18};
19
20class FanController
21 : public Loggable<FanController>
22 , public Toggleable<FanController>
23{
24public:
25 const char* className() const { return "Fan"; }
26
27 void setSpeed(uint8_t pct) {
28 if (!checkEnabled("setSpeed")) return;
29 log("setSpeed");
30 setPwm(pct);
31 }
32};
33
34FanController fan;
35fan.disable();
36fan.setSpeed(75); // выводит "[Fan] disabled, ignoring setSpeed", PWM не меняется
37fan.enable();
38fan.setSpeed(75); // выводит "[Fan] setSpeed", устанавливает PWM
Примесь 3: Comparable
Генерирует все операторы сравнения из единственного метода compareTo:
1template <typename Derived>
2class Comparable {
3public:
4 bool operator==(const Derived& other) const {
5 return self().compareTo(other) == 0;
6 }
7 bool operator!=(const Derived& other) const { return !(*this == other); }
8 bool operator< (const Derived& other) const { return self().compareTo(other) < 0; }
9 bool operator> (const Derived& other) const { return self().compareTo(other) > 0; }
10 bool operator<=(const Derived& other) const { return self().compareTo(other) <= 0; }
11 bool operator>=(const Derived& other) const { return self().compareTo(other) >= 0; }
12
13private:
14 const Derived& self() const { return static_cast<const Derived&>(*this); }
15};
16
17class Temperature : public Comparable<Temperature> {
18 float celsius_;
19public:
20 explicit Temperature(float c) : celsius_(c) {}
21
22 // Нужно реализовать только один метод
23 int compareTo(const Temperature& other) const {
24 if (celsius_ < other.celsius_) return -1;
25 if (celsius_ > other.celsius_) return 1;
26 return 0;
27 }
28
29 float value() const { return celsius_; }
30};
31
32Temperature t1(20.0f), t2(25.0f);
33if (t1 < t2) printf("colder\n"); // работает
34if (t1 != t2) printf("different\n"); // работает
Оператор <=> C++20 ещё больше сокращает этот шаблонный код, но CRTP-паттерн
Comparable работает начиная с C++11.
Примесь 4: Printable
Добавляет print() и toString() через метод describe() производного класса:
1template <typename Derived>
2class Printable {
3public:
4 void print() const {
5 auto& self = static_cast<const Derived&>(*this);
6 std::string s = self.describe();
7 printf("%s\n", s.c_str());
8 }
9
10 std::string toString() const {
11 return static_cast<const Derived&>(*this).describe();
12 }
13};
14
15class SensorReading : public Printable<SensorReading> {
16 float temp_;
17 float humid_;
18public:
19 SensorReading(float t, float h) : temp_(t), humid_(h) {}
20
21 std::string describe() const {
22 char buf[64];
23 snprintf(buf, sizeof(buf), "T=%.1f°C H=%.0f%%", temp_, humid_);
24 return buf;
25 }
26};
27
28SensorReading r(23.5f, 60.0f);
29r.print(); // выводит "T=23.5°C H=60%"
30std::string s = r.toString(); // "T=23.5°C H=60%"
Примесь 5: Countable
Отслеживает количество существующих экземпляров класса:
1template <typename Derived>
2class Countable {
3 static size_t count_;
4public:
5 Countable() { ++count_; }
6 Countable(const Countable&) { ++count_; }
7 ~Countable() { --count_; }
8
9 static size_t instanceCount() { return count_; }
10};
11
12template <typename Derived>
13size_t Countable<Derived>::count_ = 0;
14
15class Message : public Countable<Message> {
16 uint8_t id_;
17public:
18 explicit Message(uint8_t id) : id_(id) {}
19};
20
21{
22 Message m1(1), m2(2);
23 printf("%zu messages\n", Message::instanceCount()); // 2
24}
25printf("%zu messages\n", Message::instanceCount()); // 0
Поскольку Countable<Message> и Countable<Sensor> — разные типы,
у каждого класса есть свой статический счётчик.
Композиция примесей
CRTP-примеси компонуются через множественное наследование. Порядок важен только если две примеси предоставляют метод с одинаковым именем:
1class SensorDriver
2 : public Loggable<SensorDriver>
3 , public Toggleable<SensorDriver>
4 , public Printable<SensorDriver>
5 , public Countable<SensorDriver>
6{
7public:
8 const char* className() const { return "SensorDriver"; }
9
10 std::string describe() const {
11 return std::string(className()) + (isEnabled() ? " [on]" : " [off]");
12 }
13
14 void read() {
15 if (!checkEnabled("read")) return;
16 log("reading sensor");
17 // фактическое чтение
18 }
19};
20
21SensorDriver s1, s2;
22s1.disable();
23s1.read(); // "[SensorDriver] disabled, ignoring read"
24s2.read(); // "[SensorDriver] reading sensor"
25s2.print(); // "SensorDriver [on]"
26printf("%zu\n", SensorDriver::instanceCount()); // 2
Каждая примесь независима и добавляет одну ортогональную возможность. Добавление новой примеси не изменяет существующий код.
Краткий справочник
| Примесь | Метод для реализации в Derived | Добавляет |
|---|---|---|
Loggable<D> |
className() → const char* |
log(msg) |
Toggleable<D> |
необязательный className() |
enable(), disable(), isEnabled(), checkEnabled() |
Comparable<D> |
compareTo(const D&) → int |
==, !=, <, >, <=, >= |
Printable<D> |
describe() → std::string |
print(), toString() |
Countable<D> |
нет | статический instanceCount(), автоматическое отслеживание |