C++ · lesson 20 of 24
Beginner
6Intermediate
11- 5 Operators and Operator Overloading in C++
- 8 Classes and Object-Oriented Programming in C++
- 9 Inheritance and Polymorphism in C++
- 11 STL Containers in C++
- 12 STL Algorithms in C++
- 13 Smart Pointers in C++
- 15 Lambdas and Functional Programming in C++
- 18 Error Handling in C++
- 19 File I/O in C++
- 23 C++ Interview Preparation: Top 35 Questions
- 24 Static Members in C++
Advanced
7Also part of: Low-Level Design
Design Patterns in C++
C++ design patterns run for real: CRTP, mixins, policy-based design, a PIMPL build and its destructor trap, Observer, Factory, thread-safe Singleton.
By SkillByExample · Updated
C++ supports the classic object-oriented patterns, and adds compile-time ones — CRTP, policies — that resolve the “which implementation?” question during compilation instead of at runtime.
CRTP: Curiously Recurring Template Pattern
In CRTP a class derives from a base template instantiated with itself. The base can then call derived methods through a static_cast, with no virtual:
#include <iostream>
template <typename Derived>
class Shape {
public:
double area() const { return static_cast<const Derived*>(this)->area_impl(); }
void print() const { std::cout << Derived::name << " area: " << area() << "\n"; }
};
class Circle : public Shape<Circle> {
double r_;
public:
static constexpr const char* name = "circle";
explicit Circle(double r) : r_(r) {}
double area_impl() const { return 3.14159 * r_ * r_; }
};
class Square : public Shape<Square> {
double s_;
public:
static constexpr const char* name = "square";
explicit Square(double s) : s_(s) {}
double area_impl() const { return s_ * s_; }
};
struct VirtualCircle { // the runtime-polymorphic alternative
double r;
virtual ~VirtualCircle() = default;
virtual double area() const { return 3.14159 * r * r; }
};
template <typename T> void describe(const Shape<T>& s) { s.print(); } // static dispatch
int main() {
describe(Circle(5));
describe(Square(3));
std::cout << "sizeof(Circle) = " << sizeof(Circle)
<< ", sizeof(VirtualCircle) = " << sizeof(VirtualCircle) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o crtp crtp.cpp
$ ./crtp
circle area: 78.5397
square area: 9
sizeof(Circle) = 8, sizeof(VirtualCircle) = 16
Shape<Circle>::area() knows at compile time that this is really a Circle, so the call to area_impl() is direct and can be inlined. The size difference is the vtable pointer: Circle holds only its double (8 bytes), while the equivalent class with a virtual function carries a hidden pointer too (16). The cost of CRTP is that Shape<Circle> and Shape<Square> are unrelated types — you can’t put them in one std::vector — which is exactly when you reach for virtual functions instead.
CRTP also works for mixins, injecting behaviour into a class:
#include <iostream>
template <typename Derived>
struct Comparable { // derives != > <= >= from == and <
const Derived& self() const { return static_cast<const Derived&>(*this); }
bool operator!=(const Derived& o) const { return !(self() == o); }
bool operator>(const Derived& o) const { return o < self(); }
bool operator<=(const Derived& o) const { return !(o < self()); }
bool operator>=(const Derived& o) const { return !(self() < o); }
};
struct Point : Comparable<Point> {
int x, y;
Point(int x, int y) : x(x), y(y) {}
bool operator==(const Point& o) const { return x == o.x && y == o.y; }
bool operator<(const Point& o) const { return x < o.x || (x == o.x && y < o.y); }
};
int main() {
Point a{1, 2}, b{1, 5};
std::cout << std::boolalpha << "a < b " << (a < b) << ", a > b " << (a > b)
<< ", a <= b " << (a <= b) << ", a != b " << (a != b) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o mixin mixin.cpp
$ ./mixin
a < b true, a > b false, a <= b true, a != b true
Point writes only == and <; Comparable<Point> derives the other four from them. In C++20, a defaulted operator<=> does this job without a base class (see the operators lesson), but the mixin technique still applies to anything else you want to bolt on.
Policy-Based Design
A policy is a template parameter that supplies one behaviour. Here, the locking strategy of a buffer:
#include <cstddef>
#include <iostream>
#include <mutex>
struct NoLock { // policy 1: zero-cost no-op
struct Guard { explicit Guard(NoLock&) {} };
};
struct MutexLock { // policy 2: a real mutex
std::mutex m;
struct Guard { std::lock_guard<std::mutex> g; explicit Guard(MutexLock& p) : g(p.m) {} };
};
struct LoggingLock { // policy 3: for debugging
struct Guard {
explicit Guard(LoggingLock&) { std::cout << " [lock]"; }
~Guard() { std::cout << " [unlock]\n"; }
};
};
template <typename T, std::size_t N, typename LockPolicy = NoLock>
class Buffer : private LockPolicy {
T data_[N]{};
public:
void write(std::size_t i, T v) {
typename LockPolicy::Guard guard(*this); // no-op, mutex or log
data_[i] = v;
}
T read(std::size_t i) const { return data_[i]; }
};
int main() {
Buffer<int, 4> fast;
Buffer<int, 4, MutexLock> safe;
Buffer<int, 4, LoggingLock> traced;
fast.write(0, 1);
safe.write(0, 2);
traced.write(0, 3);
std::cout << "reads: " << fast.read(0) << " " << safe.read(0) << " " << traced.read(0) << "\n";
std::cout << "sizeof: NoLock " << sizeof(fast) << ", MutexLock " << sizeof(safe)
<< ", LoggingLock " << sizeof(traced) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o policy policy.cpp
$ ./policy
[lock] [unlock]
reads: 1 2 3
sizeof: NoLock 16, MutexLock 56, LoggingLock 16
The three buffers share one implementation of write, but each constructs a different Guard. With NoLock, the guard is an empty object the optimizer removes entirely, so the single-threaded buffer pays nothing — and because Buffer inherits the empty policy privately, the empty-base optimization keeps it at 16 bytes, the size of four ints. MutexLock adds a real 40-byte std::mutex (56 total). LoggingLock shows the guard running around the write. The standard library uses the same idea for allocators and comparators.
The PIMPL Idiom
PIMPL (“pointer to implementation”) hides a class’s private members in a struct that only the .cpp file defines.
// widget.h — what users include
#pragma once
#include <memory>
#include <string>
class Widget {
public:
explicit Widget(std::string name);
~Widget(); // defined in widget.cpp, where Impl is complete
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void set_color(int r, int g, int b);
void render() const;
private:
struct Impl; // declared, never defined here
std::unique_ptr<Impl> impl_;
};
// widget.cpp — the only file that sees Impl
#include "widget.h"
#include <iostream>
struct Widget::Impl {
std::string name;
int r = 0, g = 0, b = 0;
};
Widget::Widget(std::string name) : impl_(std::make_unique<Impl>(Impl{std::move(name)})) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::set_color(int r, int g, int b) { impl_->r = r; impl_->g = g; impl_->b = b; }
void Widget::render() const {
std::cout << "render " << impl_->name << " rgb(" << impl_->r << "," << impl_->g << "," << impl_->b << ")\n";
}
// pimpl_main.cpp — knows only widget.h
#include <iostream>
#include "widget.h"
int main() {
Widget w("button");
w.set_color(255, 128, 0);
w.render();
Widget moved = std::move(w);
moved.render();
std::cout << "sizeof(Widget) = " << sizeof(Widget) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o app pimpl_main.cpp widget.cpp
$ ./app
render button rgb(255,128,0)
render button rgb(255,128,0)
sizeof(Widget) = 8
pimpl_main.cpp compiled without ever seeing Impl’s members, and Widget itself is one pointer — 8 bytes — whatever Impl grows into. Adding a field to Impl recompiles only widget.cpp. The fiddly part is the destructor: it is declared in the header but defined (as = default) in widget.cpp. Move the = default into the header and see what happens:
$ sed -i 's|~Widget();.*|~Widget() = default;|' widget.h
$ sed -i '/^Widget::~Widget() = default;$/d' widget.cpp
$ g++ -std=c++20 -Wall -Wextra -o app pimpl_main.cpp widget.cpp 2>&1 | grep error
/usr/include/c++/13/bits/unique_ptr.h:97:23: error: invalid application of 'sizeof' to incomplete type 'Widget::Impl'
The destructor is now generated in pimpl_main.cpp, where Impl is only forward-declared, and std::unique_ptr’s deleter needs sizeof(Impl) to delete it. The same applies to the move assignment operator, which must destroy the old Impl.
Observer
Observer decouples an event source from its listeners. std::function lets any callable subscribe, with no listener base class:
#include <algorithm>
#include <cstddef>
#include <functional>
#include <iostream>
#include <utility>
#include <vector>
template <typename... Args>
class Event {
std::vector<std::pair<std::size_t, std::function<void(Args...)>>> handlers_;
std::size_t next_id_ = 0;
public:
std::size_t subscribe(std::function<void(Args...)> h) {
handlers_.emplace_back(next_id_, std::move(h));
return next_id_++;
}
void unsubscribe(std::size_t id) {
std::erase_if(handlers_, [id](const auto& h) { return h.first == id; });
}
void emit(Args... args) const { for (const auto& [id, h] : handlers_) h(args...); }
};
struct Button {
Event<int, int> on_click;
void click(int x, int y) { std::cout << "click(" << x << "," << y << ")\n"; on_click.emit(x, y); }
};
int main() {
Button btn;
int clicks = 0;
auto logger = btn.on_click.subscribe([](int x, int y) { std::cout << " log: " << x << "," << y << "\n"; });
btn.on_click.subscribe([&clicks](int, int) { std::cout << " counter: " << ++clicks << "\n"; });
btn.click(10, 20);
btn.on_click.unsubscribe(logger);
btn.click(30, 40);
}
$ g++ -std=c++20 -Wall -Wextra -o observer observer.cpp
$ ./observer
click(10,20)
log: 10,20
counter: 1
click(30,40)
counter: 2
Both handlers ran for the first click. subscribe returns an id, so the logger could remove itself; the second click reached only the counter. The counter lambda captures clicks by reference, which is fine here because clicks outlives btn — a handler that captures something shorter-lived is the classic Observer dangling bug.
Factory
A factory maps a runtime value — a config string, a file extension — to a concrete type, so callers never name the concrete classes.
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
struct Serializer {
virtual ~Serializer() = default;
virtual std::string serialize(const std::string& data) const = 0;
static std::unique_ptr<Serializer> create(const std::string& format);
};
struct Json : Serializer {
std::string serialize(const std::string& d) const override { return "{\"data\": \"" + d + "\"}"; }
};
struct Xml : Serializer {
std::string serialize(const std::string& d) const override { return "<data>" + d + "</data>"; }
};
std::unique_ptr<Serializer> Serializer::create(const std::string& format) {
static const std::map<std::string, std::function<std::unique_ptr<Serializer>()>> registry{
{"json", [] { return std::make_unique<Json>(); }},
{"xml", [] { return std::make_unique<Xml>(); }},
};
auto it = registry.find(format);
if (it == registry.end()) throw std::invalid_argument("unknown format: " + format);
return it->second();
}
int main() {
for (const char* fmt : {"json", "xml", "yaml"}) {
try {
auto s = Serializer::create(fmt); // may throw before anything prints
std::cout << fmt << " -> " << s->serialize("hello") << "\n";
} catch (const std::invalid_argument& e) {
std::cout << fmt << " -> error: " << e.what() << "\n";
}
}
}
$ g++ -std=c++20 -Wall -Wextra -o factory factory.cpp
$ ./factory
json -> {"data": "hello"}
xml -> <data>hello</data>
yaml -> error: unknown format: yaml
Adding a format means adding one registry entry; callers don’t change. The registry is a function-local static, built once on first use. Unknown formats throw instead of returning null, so a misspelled config value fails loudly. Note the create call is on its own line: in std::cout << fmt << " -> " << Serializer::create(fmt)..., C++17 evaluates the << chain left to right, so “yaml -> ” would already be printed when create throws.
Thread-Safe Singleton
Since C++11, a function-local static is initialized exactly once even when threads race to reach it — the “Meyers singleton”. Eight threads race here:
#include <atomic>
#include <iostream>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
std::atomic<int> constructions{0};
class Config {
std::unordered_map<std::string, std::string> settings_;
Config() { ++constructions; settings_["version"] = "1.0"; } // only instance() can call
public:
static Config& instance() {
static Config inst; // initialized exactly once, thread-safe since C++11
return inst;
}
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
const std::string& get(const std::string& k) const { return settings_.at(k); }
};
int main() {
std::vector<std::thread> threads;
std::vector<const Config*> seen(8);
for (int i = 0; i < 8; ++i)
threads.emplace_back([&seen, i] { seen[i] = &Config::instance(); }); // race to be first
for (auto& t : threads) t.join();
bool same = true;
for (auto* p : seen) same = same && (p == seen[0]);
std::cout << std::boolalpha << "constructed " << constructions << " time(s), all threads got the same object: "
<< same << ", version " << Config::instance().get("version") << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o singleton singleton.cpp
$ ./singleton
constructed 1 time(s), all threads got the same object: true, version 1.0
One construction, one address shared by all eight threads, with no mutex in sight — the compiler emits a guarded one-time initialization. That guarantee covers construction only; if Config had setters, concurrent calls would still need a lock. Singletons are global state, which makes testing harder, so prefer passing a Config& to the code that needs it and keep singletons for truly process-wide things like a logger.
Practice
-
Add a
Triangletocrtp.cppand a function templatetotal_area(const Shape<Ts>&... shapes)that sums any number of shapes.Solution
class Triangle : public Shape<Triangle> { double b_, h_; public: static constexpr const char* name = "triangle"; Triangle(double b, double h) : b_(b), h_(h) {} double area_impl() const { return 0.5 * b_ * h_; } }; template <typename... Ts> double total_area(const Shape<Ts>&... shapes) { return (shapes.area() + ...); } // total_area(Circle(1), Square(2), Triangle(3, 4)) -> 13.1416A variadic template is how CRTP handles “several different shapes” without a common runtime base.
-
Write a
CountingLockpolicy forpolicy.cppwhose guard increments a counter, and print how many writes a buffer received.Solution
struct CountingLock { int count = 0; struct Guard { explicit Guard(CountingLock& p) { ++p.count; } }; }; // In Buffer, add: int lock_count() const { return LockPolicy::count; } // (only valid for policies that have a count)Because the policy is a private base,
Buffercan read its members; member functions of a class template are only instantiated when called, solock_count()doesn’t break the other policies. -
Add a
std::string label() constaccessor toWidgetwithout touchingwidget.h’s private section.Solution
Declare
std::string label() const;in the public section ofwidget.hand define it inwidget.cppasreturn impl_->name;. The private section — oneunique_ptr— never changes, which is the point:sizeof(Widget)stays 8. -
Make an
Eventsubscriber that unsubscribes itself after its first call.Solution
std::size_t once_id = 0; once_id = btn.on_click.subscribe([&](int, int) { std::cout << " once\n"; btn.on_click.unsubscribe(once_id); // modifies handlers_ during emit! });This is a trap:
unsubscribeerases from the vector thatemitis iterating, which is undefined behavior. A robustEventdefers removals untilemitfinishes, or iterates over a copy of the handler list. -
Register a
"csv"serializer infactory.cppthat returnsdata\r\nhello.Solution
struct Csv : Serializer { std::string serialize(const std::string& d) const override { return "data\r\n" + d; } }; // registry: {"csv", [] { return std::make_unique<Csv>(); }},maindoesn’t change at all — only the registry grows.