C++ Module 23 – Design, idioms and maintainable C++
CRTP, mixins and static polymorphism
Use the curiously recurring template pattern (CRTP) to add operators, counters and other mixins to C++ classes, and compare it with virtual calls.
What you will learn
- Use the curiously recurring template pattern to call derived-class code from a base
- Compose a class's behaviour from small CRTP mixins
- Compare static polymorphism with virtual functions
- Prevent a mixin from being attached to the wrong class
- Choose between CRTP, deducing this and concepts for the same job
Before you start
What you’ll learn · Before you start
What you will learn
- Use the curiously recurring template pattern to call derived-class code from a base
- Compose a class's behaviour from small CRTP mixins
- Compare static polymorphism with virtual functions
- Prevent a mixin from being attached to the wrong class
- Choose between CRTP, deducing this and concepts for the same job
Before you start
On this page
Employee and Version are unrelated classes, and both need comparisons, a printed description and a count of
live objects: written by hand, that is six sets of functions to keep in step. The
curiously recurring template pattern (CRTP): A class template used as a base, to which each derived class passes itself as the template argument, as in struct Square : DescribableCrtp<Square>, so that the base can cast *this to the derived type while compiling. More in “Deducing this: explicit object parameters in C++23”, CRTP for short, writes each set once: a class
derives from a base template instantiated with itself, class Employee : public KeyOrdered<Employee>, so the base
knows the exact derived class. It can call that class’s member functions, take it as a parameter type and keep state
for it, all without virtual functions.
mixin: A small base class that adds one capability to the classes that derive from it, such as a count of live objects, with the few members it needs, so that it can be combined with other bases (C++ Core Guidelines C.136). More in “Multiple inheritance, the diamond and virtual bases” are small CRTP bases that each add one capability, and a class collects the mixins it needs. The result is static polymorphism: Code that works with many types, where the compiler chooses which function each call reaches from the types it sees, as with templates and CRTP bases. The call can then be inlined, but objects of different classes share no common type to be stored under.: the compiler chooses which function each call reaches while it compiles, so the call can be inlined. Dynamic polymorphism, with virtual functions, makes that choice at run time, which is what lets one container hold different classes. Every program below ran with Apple clang 21 and GCC 16.
A CRTP base knows its derived class while compiling
A base template Describable<D> cannot know D’s members while D itself is still incomplete, but its member
functions are compiled only when they are first used, and by then D is a complete class. So describe() can turn
*this into the derived object with static_cast<const D&>(*this) and call name() and details() on it. The cast
is a compile-time conversion from base to derived, which is correct because the object really is a D. Step through
one call of describe() on an Employee:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The class names itself. Employee derives from Describable<Employee>: it passes its own name as the template argument D. While the base template was written, D was only a name.
- Compiled on first use. The member functions of Describable<Employee> are compiled only when they are first used. By then Employee is a complete class, with name() and details() declared.
- asha.describe(). The call finds describe() in the Describable<Employee> part of asha. Nothing is looked up at run time: the compiler knows that asha is an Employee.
- The cast down. static_cast<const Employee&>(*this) turns the base part into the whole Employee. The compiler checks only that Employee derives from the base; it is right because the object really is an Employee.
- The derived functions. self.name() and self.details() are ordinary calls to Employee's own functions, chosen while compiling, so the compiler can inline them. No virtual function and no vtable pointer are involved.
- The result. std::format joins the two parts, and the call returns "Asha (employee 1042)", the text both compilers printed.
Operators work the same way: KeyOrdered<D> defines == and <=> as friends that take two const D&, so an
Employee can only be compared with another Employee, and C++20 derives !=, <, <=, > and >= from those
two.
Key idea
The base learns the derived class from its own template argument, and reaches it through
static_cast<const D&>(*this), a cast the compiler checks against the class hierarchy only.
Checkpoint
Check: How a CRTP base reaches its derived class
Three mixins give two unrelated classes the same abilities
Employee and Version share no base class of their own; each lists the three mixins it wants and provides the
members they need:
C header · mixins.h
// mixins.h: three CRTP mixins. Each one gives a class D one capability, built from members that D provides.
#pragma once
#include <format>
#include <string>
template <class D>
class KeyOrdered { // needs D::key(); gives ==, !=, <, <=, > and >= between two D objects
public:
friend bool operator==(const D& a, const D& b) { return a.key() == b.key(); }
friend auto operator<=>(const D& a, const D& b) { return a.key() <=> b.key(); }
};
template <class D>
class Describable { // needs D::name() and D::details(); gives describe()
public:
std::string describe() const {
const D& self = static_cast<const D&>(*this); // this object is a D, so the cast is safe
return std::format("{} ({})", self.name(), self.details());
}
};
template <class D>
class Counted { // gives D::live(): how many D objects exist right now
public:
static int live() { return count; }
protected:
Counted() { ++count; }
Counted(const Counted&) { ++count; }
Counted& operator=(const Counted&) = default;
~Counted() { --count; }
private:
inline static int count = 0; // one counter for each D: Counted<D> is a different class for every D
}; C++ · crtp.cpp
// crtp.cpp: two unrelated classes pick up comparisons, describe() and a live count from the same three mixins.
#include "mixins.h"
#include <algorithm>
#include <print>
#include <string>
#include <tuple>
#include <vector>
class Employee : public KeyOrdered<Employee>, public Describable<Employee>, public Counted<Employee> {
public:
Employee(int id, std::string name) : id_(id), name_(std::move(name)) {}
int key() const { return id_; }
std::string name() const { return name_; }
std::string details() const { return std::format("employee {}", id_); }
private:
int id_;
std::string name_;
};
class Version : public KeyOrdered<Version>, public Describable<Version>, public Counted<Version> {
public:
Version(int major, int minor, int patch) : major_(major), minor_(minor), patch_(patch) {}
std::tuple<int, int, int> key() const { return {major_, minor_, patch_}; }
std::string name() const { return std::format("v{}.{}.{}", major_, minor_, patch_); }
std::string details() const { return patch_ == 0 ? "feature release" : "fix release"; }
private:
int major_, minor_, patch_;
};
int main() {
std::vector<Version> versions{{2, 10, 1}, {2, 9, 0}, {2, 10, 0}};
std::ranges::sort(versions); // uses the < that KeyOrdered<Version> provides
for (const Version& v : versions) std::println("{}", v.describe());
const Employee asha{1042, "Asha"};
const Employee ravi{1007, "Ravi"};
std::println("{} < {}: {}", asha.describe(), ravi.describe(), asha < ravi);
std::println("live objects: {} Employee, {} Version", Employee::live(), Version::live());
std::println("sizeof(Employee) = {}, sizeof(Version) = {}", sizeof(Employee), sizeof(Version));
} from session import run
# Three CRTP mixins on two unrelated classes, with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra crtp.cpp -o crtp && ./crtp
g++-16 -std=c++23 -Wall -Wextra crtp.cpp -o crtp-gcc && ./crtp-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra crtp.cpp -o crtp && ./crtp v2.9.0 (feature release) v2.10.0 (feature release) v2.10.1 (fix release) Asha (employee 1042) < Ravi (employee 1007): false live objects: 2 Employee, 3 Version sizeof(Employee) = 32, sizeof(Version) = 12 $ g++-16 -std=c++23 -Wall -Wextra crtp.cpp -o crtp-gcc && ./crtp-gcc v2.9.0 (feature release) v2.10.0 (feature release) v2.10.1 (fix release) Asha (employee 1042) < Ravi (employee 1007): false live objects: 2 Employee, 3 Version sizeof(Employee) = 40, sizeof(Version) = 12
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 crtp_session.py
Step through main() and watch the two counters and the order of the versions:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Three versions. The vector holds three Versions. Counted<Version> adds one for every Version built, copies included, and takes one off for every Version destroyed, so once the vector is ready its count is 3.
- Sorted by key(). std::ranges::sort uses the < that KeyOrdered<Version> gives. It compares the tuples that key() returns, so 2.9.0 comes before 2.10.0, as numbers do, not as text does.
- describe() for each. Describable<Version> reaches each Version's name() and details(): "v2.9.0 (feature release)", "v2.10.0 (feature release)", "v2.10.1 (fix release)".
- Two employees. asha (1042) and ravi (1007) are built. Counted<Employee> is a different class from Counted<Version>, with a counter of its own, which reaches 2.
- asha < ravi. KeyOrdered<Employee> compares the keys 1042 and 1007, and the line prints false. Only two Employees can be compared: the operators take const Employee&.
- The live counts. The line "live objects: 2 Employee, 3 Version" reads both counters: one per class, because each Counted<D> is a class of its own.
- No bytes for mixins. sizeof(Version) is 12, its three ints: the three empty bases take no space. An Employee is 32 bytes with libc++ and 40 with libstdc++, an int and a std::string, whose size differs between the two.
std::ranges::sort sorted the versions numerically, 2.9.0 before 2.10.0, because KeyOrdered compares the tuple
that key() returns, not the printed text. describe() reached each class’s own name() and details(). The live
counts are 2 and 3 because Counted<Employee> and Counted<Version> are two different classes, each with its own
static data member: A data member declared static: one variable for the whole program, shared by every object of the class and stored in none of them, so sizeof does not count it. Since C++17, inline static declares and defines it inside the class. More in “C++ static members, friends and nested types”. And the mixins cost no memory: a Version is 12 bytes, exactly
its three int members, because the empty bases take no space. Only Counted runs code of its own: one increment
and one decrement per object.
The pattern has costs as well. Every class gets its own copy of each mixin function it uses, compiled from a header,
so a mixin used by fifty classes is instantiated fifty times, and every file that includes the header compiles it
again. A class that lacks a member a mixin needs, such as key(), fails to compile inside the mixin’s code, far from
the class itself. Mixins pay off when several classes need the same capability and the code must be exact about
their types or fast.
Checkpoint
Check: Mixins built from CRTP bases
Static polymorphism chooses while compiling, virtual calls at run time
The diagram follows one call of each kind. With CRTP, the compiler sees that asha is an Employee and compiles
describe() for exactly that class; with a virtual function, the program reads the object’s
virtual table: A table of function addresses that GCC and Clang build once for each class with virtual functions, shared by all its objects. A virtual call reads the object's vptr, takes the function's entry from this table and jumps to it. More in “Virtual functions, override and virtual destructors” when the call happens:
One call chosen while compiling, one chosen at run time
Text description of the diagram
The diagram has two parts, one above the other, each a column of three boxes joined by arrows.
The upper part is titled "Chosen while compiling: CRTP". 1. The call asha.describe(), where asha is an Employee. 2. Describable<Employee>::describe(), the base compiled for Employee, which uses static_cast<const Employee&>(*this). 3. Employee::name() and details(), called directly, so they can be inlined.
The lower part is titled "Chosen at run time: a virtual function". 1. The call shape->area(), where shape is a const Shape*. 2. The program reads the object's vtable pointer, then the slot for area(). 3. Square::area(), reached by an indirect call.
| Criterion | Virtual | CRTP | Deducing this | Concept |
|---|---|---|---|---|
| The function is chosen | At run time | While compiling | While compiling | While compiling |
| A base class is needed | Yes, the interface | Yes, a template of D | Yes, an ordinary class | No |
| One container for all classes | Yes, through pointers | No | No | No |
| Its own state per class | No | Yes | No | No |
Each column is a real alternative for a different situation. Virtual functions are the tool when code must handle objects whose class is known only at run time, such as plugins or a list of mixed shapes. CRTP fits when the class is known while compiling and the shared code needs the exact derived type. Deducing this, the explicit object parameter: A first parameter of a member function marked with this, as in int get(this const Counter& self) (C++23). It receives the object the function is called on; the function has no implicit this, and the parameter's type can be deduced like any template parameter. More in “Deducing this: explicit object parameters in C++23” of C++23, lets an ordinary base deduce the derived type at each call, which replaces many CRTP uses with simpler code. Concepts describe the members a type must have, so generic code needs no base class at all. A CRTP mixin can also be attached to the wrong class by a copy-paste slip, and that slip still compiles.
Checkpoint
Check: Static polymorphism and virtual calls
Practise: choose static or dynamic
Ten questions on the three ideas above follow. The session starts where your answers have been weakest so far, and a wrong choice shows what it overlooked.
Practice that adapts to you
Practise this lesson
10 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
CRTP mistakes reproduced, and its speed measured against virtual calls Pro: opens with a Pro, Premium or Ultimate pass
Two things decide whether CRTP is worth it in a code base: how it fails when it is misused, and what it buys at run time. This part reproduces the most common slip and turns it into a compile error, then reads the code that both compilers generate for a CRTP call and a virtual call, and times them.
In this part:
- A mixin attached to the wrong class, reproduced and then refused by the compiler
- What a CRTP call compiles to, measured against a virtual call
1 diagram · 5 runnable examples (C header, C++, C++ (wrong_base.cpp), C++ (wrong_base_guarded.cpp), Python) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
CRTP, deducing this or a concept: one mixin three ways, then an exercise Pro: opens with a Pro, Premium or Ultimate pass
C++23 and C++20 offer two alternatives to a CRTP base, and each one changes what the compiler checks and reports. This part writes the same comparison mixin three ways, compares the errors each one gives, shows the job that only CRTP can do, and ends with a refactoring exercise.
In this part:
- The same comparison mixin written three ways
- What each way reports when a class forgets key()
- Where CRTP is still the only tool: per-class state and constructors
- Your turn: refactor Incrementable to deducing this
1 diagram · 6 runnable examples (C header, C++, Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
A warm-up answered in full, then three design questions about a wrong base, a library’s interface and C++23:
-
Fresher to mid · Warm-up
What is CRTP, and what does it give you that a virtual function does not?
CRTP, the curiously recurring template pattern, is a class that derives from a base template instantiated with itself, as in class Employee : public KeyOrdered<Employee>. The base then knows the exact derived type: it can call the derived class's member functions through static_cast<const D&>(*this), declare operators that take const D&, and keep a separate static member for every derived class. Each call is chosen while compiling, so the compiler can inline it, and the object needs no vtable pointer. What it gives up is run-time choice: each derived class has a different base, so there is no common type through which one container could hold Employee and Version objects together. For that, virtual functions are still the tool.
-
Mid to senior
A class derived from the wrong instantiation of a CRTP base, Receipt : Describable<Invoice>, compiles and prints another class's data. Explain why it compiles, and how you would make that mistake a compile error wherever the base is used.
-
Senior to staff
You are designing the shape interface of a geometry library used by other teams. When would you expose it as a CRTP base, and when as an abstract class with virtual functions? Give the costs you would weigh, with numbers where you can.
-
Senior to staff
A CRTP base gives every derived class its own instance counter and a clone() that returns std::unique_ptr<D>. The team wants to replace it with a C++23 base that uses explicit object parameters. Which parts can move, and which cannot? Explain each.
Key takeaways
- In CRTP, a class derives from a base template instantiated with itself, so the base can use the exact derived
type:
static_cast<const D&>(*this)reaches its members, and its operators can takeconst D&. - Mixins are small CRTP bases that each add one capability; empty mixins add no bytes to the object, and each
Base<D>is a separate class, so it can keep separate static data for every derived class. - Static polymorphism chooses the function while compiling, which allows inlining; virtual functions choose at run time, which allows one container for many classes.
- Deducing this and concepts do many CRTP jobs with simpler code, and virtual functions remain the tool for classes known only at run time.
Terms in this lesson
- static polymorphism
- Code that works with many types, where the compiler chooses which function each call reaches from the types it sees, as with templates and CRTP bases. The call can then be inlined, but objects of different classes share no common type to be stored under.
References
- Working Draft, Programming Languages: C++ (N4950): [expr.static.cast], [class.friend], [over.match.oper], [dcl.fct] (explicit object parameters) (ISO/IEC JTC1/SC22/WG21)
- P0847R7: Deducing this (ISO/IEC JTC1/SC22/WG21)
- Curiously Recurring Template Pattern (cppreference.com)
- C++ Core Guidelines: T.5, combine generic and OO techniques to amplify their strengths, not their costs (Standard C++ Foundation)
- C++ Core Guidelines: C.129, when designing a class hierarchy, distinguish between implementation inheritance and interface inheritance (Standard C++ Foundation)
- UndefinedBehaviorSanitizer (-fsanitize=vptr) (LLVM Project)
- Program Instrumentation Options (-fsanitize=vptr, -fsanitize=undefined) (GNU Project)
- Options That Control Optimization (-fdevirtualize-speculatively) (GNU Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress