Что такое 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(), автоматическое отслеживание