What a CRTP mixin is

A CRTP mixin adds behaviour to a class by making it inherit from a template base that gets the concrete class as its type parameter:

1template <typename Derived>
2class Mixin {
3    // can call Derived methods via static_cast<Derived*>(this)
4};
5
6class MyClass : public Mixin<MyClass> {
7    // MyClass now has everything Mixin provides
8};

No virtual dispatch. No vtable. The mixin is inlined at compile time.


Mixin 1: Loggable

Adds a log(msg) method that prefixes the class name:

 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};

If the derived class doesn’t provide className(), the compiler gives an error at the point where log is first called — no runtime crash.


Mixin 2: Toggleable

Adds enable/disable state:

 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    // Call this from derived methods that should be no-ops when disabled
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);  // prints "[Fan] disabled, ignoring setSpeed", no PWM change
37fan.enable();
38fan.setSpeed(75);  // prints "[Fan] setSpeed", sets PWM

Mixin 3: Comparable

Generates all comparison operators from a single compareTo method:

 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    // Only one method to implement
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");    // works
34if (t1 != t2) printf("different\n"); // works

C++20’s operator<=> reduces this boilerplate further with the spaceship operator, but the CRTP Comparable pattern works in C++11 and later.


Mixin 4: Printable

Adds print() and toString() using a derived describe() method:

 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();                      // prints "T=23.5°C H=60%"
30std::string s = r.toString();   // "T=23.5°C H=60%"

Mixin 5: Countable

Tracks how many instances of a class exist:

 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

Because Countable<Message> and Countable<Sensor> are different types, each class has its own static counter.


Composing mixins

CRTP mixins compose via multiple inheritance. Order matters only if two mixins provide the same method name:

 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        // actual read
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

Each mixin is independent and adds one orthogonal capability. Adding a new mixin doesn’t change any existing code.


Quick reference

Mixin Method to implement in Derived Adds
Loggable<D> className()const char* log(msg)
Toggleable<D> optional className() enable(), disable(), isEnabled(), checkEnabled()
Comparable<D> compareTo(const D&)int ==, !=, <, >, <=, >=
Printable<D> describe()std::string print(), toString()
Countable<D> none instanceCount() static, auto-tracked