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 |