ch9_patterns_and_idioms
Ch 9. Implementing Patterns and Idioms
Avoiding repetitive if-else statements in factory patterns
-
Problem with repetitive
if...elsein factory patterns- Leads to hard-to-maintain and error-prone code as conditions increase.
- Common in object creation logic (e.g., image format handlers).
-
Initial implementation using
if...elsestruct IImageFactory { virtual std::unique_ptr<Image> Create(std::string_view type) = 0; }; struct ImageFactory : public IImageFactory { std::unique_ptr<Image> Create(std::string_view type) override { if (type == "bmp") return std::make_unique<BitmapImage>(); else if (type == "png") return std::make_unique<PngImage>(); else if (type == "jpg") return std::make_unique<JpgImage>(); return nullptr; } }; -
Refactored implementation using a
mapof functions- Eliminates repetition by mapping format strings to factory lambdas.
struct ImageFactory : public IImageFactory { std::unique_ptr<Image> Create(std::string_view type) override { static std::map<std::string, std::function<std::unique_ptr<Image>()>> mapping { { "bmp", []() { return std::make_unique<BitmapImage>(); } }, { "png", []() { return std::make_unique<PngImage>(); } }, { "jpg", []() { return std::make_unique<JpgImage>(); } } }; auto it = mapping.find(type.data()); if (it != mapping.end()) return it->second(); return nullptr; } }; -
How it works
- The
mappingassociates a string key (image format) with a lambda that creates an image object. - Lambdas return
std::unique_ptr<Image>, enabling polymorphism. - The factory method looks up the type and invokes the associated lambda.
- The
-
Client usage remains unchanged
auto factory = ImageFactory{}; auto image = factory.Create("png"); -
Alternative using
typeidinstead of strings- Reduces string-based errors by using
std::type_info. - Static
mappingusestype_info*as key and lambdas as values.
struct IImageFactoryByType { virtual std::unique_ptr<Image> Create(const std::type_info& type) = 0; }; struct ImageFactoryByType : public IImageFactoryByType { std::unique_ptr<Image> Create(const std::type_info& type) override { auto it = mapping.find(&type); if (it != mapping.end()) return it->second(); return nullptr; } private: static std::map<const std::type_info*, std::function<std::unique_ptr<Image>()>> mapping; }; std::map<const std::type_info*, std::function<std::unique_ptr<Image>()>> ImageFactoryByType::mapping { { &typeid(BitmapImage), []() { return std::make_unique<BitmapImage>(); } }, { &typeid(PngImage), []() { return std::make_unique<PngImage>(); } }, { &typeid(JpgImage), []() { return std::make_unique<JpgImage>(); } } }; - Reduces string-based errors by using
-
Client usage with type-based factory
auto factory = ImageFactoryByType{}; auto image = factory.Create(typeid(PngImage)); -
Considerations
- Using a
mapsimplifies adding new image types. - Static map incurs a memory cost, relevant in constrained environments.
- Type-based keys can be more reliable than string-based keys.
- Using a
Great question — the use of a pointer to std::type_info (&typeid(...)) as the key in the std::map is a nuanced design choice. Let's break it down:
✅ Why use &typeid(T) instead of just typeid(T)?
Because std::type_info does not define a copyable or movable interface, and it lacks operator<, so it can't be directly used as a map key unless you use a custom comparator.
Using pointers to type_info (const std::type_info*) gets around that because:
- Pointers are cheap to compare (just compare addresses),
- Pointers have a well-defined operator<, so the map works out-of-the-box.
🧠 Is &typeid(T) stable and unique?
Yes — &typeid(T) returns a stable and unique address per type for the lifetime of the program. That makes it ideal as a key:
&typeid(BitmapImage) == &typeid(BitmapImage) // always true
🧪 What's the danger of not using a pointer?
If you instead tried to store std::type_info directly:
std::map<std::type_info, std::function<...>> // ❌ won't compile
This fails because std::type_info:
- Is not copyable (its copy constructor is deleted),
- Doesn’t define a natural ordering (no
<operator), - Cannot be value-inserted into
std::map.
So, this is one of the rare use-cases where you must use a pointer, unless you go for a different container (e.g., unordered_map<std::type_index, T>
💡Alternative: Use std::type_index
If you want to avoid using raw pointers, there's a more modern and readable alternative:
#include <typeindex>
std::map<std::type_index, std::function<std::unique_ptr<Image>()>> mapping {
{ typeid(BitmapImage), []{ return std::make_unique<BitmapImage>(); } },
{ typeid(PngImage), []{ return std::make_unique<PngImage>(); } },
{ typeid(JpgImage), []{ return std::make_unique<JpgImage>(); } }
};
std::unique_ptr<Image> Create(const std::type_info& type) override {
auto it = mapping.find(type);
if (it != mapping.end()) return it->second();
return nullptr;
}
🔁 std::type_index is a wrapper around type_info* that defines operator== and operator<, making it usable as a map key.
Implementing the pimpl idiom
-
Purpose of the pimpl idiom
- Separates interface from implementation using an opaque pointer.
- Minimizes recompilation when implementation details change.
- Supports binary compatibility for ABI-stable libraries.
-
Initial
controlclass (pre-refactoring)- Contains internal members:
std::string text,int width,int height,bool visible
- Member functions:
draw(),set_text(),resize(),show(),hide()
- Drawing simulates console output of properties.
- Contains internal members:
-
Step-by-step refactoring using pimpl
-
Move private members to separate class
class control_pimpl; // Forward declaration in control.h -
Use
std::unique_ptrwith custom deleter incontrolclassclass control { std::unique_ptr<control_pimpl, void(*)(control_pimpl*)> pimpl; public: control(); void set_text(std::string_view); void resize(int, int); void show(); void hide(); }; -
Define
control_pimplin control.cppclass control_pimpl { std::string text; int width = 0, height = 0; bool visible = true; void draw() { std::cout << "control\n" << " visible: " << std::boolalpha << visible << std::noboolalpha << '\n' << " size: " << width << ", " << height << '\n' << " text: " << text << '\n'; } public: void set_text(std::string_view t) { text = t.data(); draw(); } void resize(int w, int h) { width = w; height = h; draw(); } void show() { visible = true; draw(); } void hide() { visible = false; draw(); } }; -
Construct
pimplincontrolconstructorcontrol::control() : pimpl(new control_pimpl(), [](control_pimpl* p) { delete p; }) {} -
Redirect public methods to
pimplvoid control::set_text(std::string_view text) { pimpl->set_text(text); } void control::resize(int w, int h) { pimpl->resize(w, h); } void control::show() { pimpl->show(); } void control::hide() { pimpl->hide(); }
-
-
Why use custom deleter with
unique_ptrcontrol_pimplis incomplete in header at declaration site.- Custom deleter allows compilation without complete type.
- Alternative: define destructor in
.cppafter full type is visible.
-
Advantages
- Interface and implementation are decoupled.
- Changes in implementation don't require recompiling users.
- Shorter build times due to reduced header dependencies.
- Clean, stable headers for library consumers.
-
Disadvantages
- Slight runtime overhead from indirection.
- More boilerplate and harder to navigate.
- Doesn't support private/protected or virtual members directly.
control_pimplmay need pointer back tocontrolif bidirectional communication is required.
-
Copyable and movable variant (
control_copyable)- Adds copy and move constructors/assignment operators.
- Deep copies
control_pimplin copy operations.
class control_copyable { std::unique_ptr<control_pimpl, void(*)(control_pimpl*)> pimpl; public: control_copyable(); control_copyable(control_copyable&&) noexcept; control_copyable& operator=(control_copyable&&) noexcept; control_copyable(const control_copyable&); control_copyable& operator=(const control_copyable&); void set_text(std::string_view); void resize(int, int); void show(); void hide(); }; control_copyable::control_copyable() : pimpl(new control_pimpl(), [](control_pimpl* p) { delete p; }) {} control_copyable::control_copyable(control_copyable&&) noexcept = default; control_copyable& control_copyable::operator=(control_copyable&&) noexcept = default; control_copyable::control_copyable(const control_copyable& op) : pimpl(new control_pimpl(*op.pimpl), [](control_pimpl* p) { delete p; }) {} control_copyable& control_copyable::operator=(const control_copyable& op) { if (this != &op) { pimpl = std::unique_ptr<control_pimpl, void(*)(control_pimpl*)>( new control_pimpl(*op.pimpl), [](control_pimpl* p) { delete p; }); } return *this; } -
General rule for pimpl
- Move all private non-virtual data/functions to
pimpl. - Leave virtual, protected, and base-related members in the public class.
- Move all private non-virtual data/functions to
-
Problem with positional parameters in C++
- Arguments must be passed in order.
- Cannot skip intermediate parameters when using default values.
- Leads to unclear or error-prone code, especially with many optional parameters.
-
Named parameter idiom: emulating named arguments
- Makes use of a helper class to encapsulate parameters.
- Provides setter-style methods for optional parameters, allowing chained calls in any order.
Implementing the named parameter idiom
Original Class with Positional Parameters
class control {
int id_;
std::string text_;
int width_;
int height_;
bool visible_;
public:
control(
int const id,
std::string_view text = "",
int const width = 0,
int const height = 0,
bool const visible = false
) : id_(id), text_(text), width_(width), height_(height), visible_(visible) {}
};
Refactored Using Named Parameter Idiom
-
Parameter holder class
class control_properties { int id_; std::string text_; int width_ = 0; int height_ = 0; bool visible_ = false; friend class control; public: control_properties(int const id) : id_(id) {} control_properties& text(std::string_view t) { text_ = t.data(); return *this; } control_properties& width(int const w) { width_ = w; return *this; } control_properties& height(int const h) { height_ = h; return *this; } control_properties& visible(bool const v) { visible_ = v; return *this; } }; -
Constructor modified to accept property object
class control { int id_; std::string text_; int width_; int height_; bool visible_; public: control(control_properties const& cp) : id_(cp.id_), text_(cp.text_), width_(cp.width_), height_(cp.height_), visible_(cp.visible_) {} };
Usage
-
Positional style (original):
control c(1044, "sample", 100, 20, true); -
Named parameter idiom:
control c(control_properties(1044) .visible(true) .height(20) .width(100));
Notes
- Required parameters are passed to the constructor of
control_properties. - Optional parameters are set using member functions that:
- Modify internal state.
- Return a reference to enable chaining.
friend class control;allows direct access to internal members without public getters.- Enhances readability and flexibility when many optional parameters exist.
Trade-offs
Pros:
- Improved clarity and flexibility.
- Allows setting only needed parameters.
- Enables setting in any order.
- No need to modify the main class for minor parameter additions.
Cons:
- More boilerplate code.
- Additional class introduced.
friendusage limits encapsulation ofcontrol_properties.- Cannot be used directly with constructors needing many positional arguments.
Related idioms
- Non-virtual interface idiom: separates public interface from implementation details.
- Attorney-client idiom: restricts friend class access to selected private members.
Separating interfaces and implementations with the non-virtual interface idiom
- Purpose of the Non-Virtual Interface (NVI) Idiom
- Separates public interface from implementation details.
- Prevents derived classes from overriding public behavior unexpectedly.
- Uses public non-virtual methods and non-public virtual methods.
Design Guidelines (Herb Sutter)
- Public interfaces should be non-virtual.
- Virtual functions should be private.
- Use protected virtual functions only when base class implementations are intended to be called from derived classes.
- Destructor rules:
- Public and virtual if deletion is polymorphic.
- Protected and non-virtual if deletion is non-polymorphic.
❓ Can a derived class override a private virtual function?
✅ Yes, absolutely.
C++ allows overriding private virtual functions from the base class — even though they are private — because:
- Access control (like
private,protected) is enforced during name lookup, not during overriding. - Virtual function overriding is based on signature matching (not visibility).
In other words: you can't call a private base class function, but you can override it.
🎯 The Key Insight: Overriding does not rely on name lookup.
It relies on matching the function signature during virtual table construction, not during normal name visibility resolution.
Let’s clarify: Name Lookup vs Overriding — Two Different Phases
| Concept | Description |
|---|---|
| Name Lookup | Happens when the compiler tries to resolve a function call (like paint() in user code). Visibility (private, protected, public) matters here. |
| Overriding | Happens during class definition — the compiler matches the function’s signature to a base class virtual function regardless of access control. |
class Base {
private:
virtual void foo(); // private virtual
};
class Derived : public Base {
void foo() override; // ✅ allowed
};
Base::foo()is private, so you can't call it from Derived.- But when the compiler sees
Derived::foo(), it checks:- “Is there a virtual function in a base class with the same signature?”
- Yes → so
Derived::foo()overridesBase::foo()— even ifBase::foo()is private.
- This matching does not require name lookup — it’s part of the class layout logic.
🔁 Why doesn’t this break access control?
Because C++'s access control is meant to prevent certain code from using a member, not from defining an override.
Overriding is allowed because:
- It doesn't call or access the base function.
- It merely says, “When someone calls this virtual function through a base class reference/pointer, call my version.”
🚫 What the derived class cannot do:
class button : public control {
public:
void foo() {
paint(); // ❌ Error: 'paint' is private in 'control'
}
};
That would result in a compiler error, because paint() is not accessible — even though it’s a virtual function.
Summary
| Operation | Allowed? | Notes |
|---|---|---|
| Override private virtual | ✅ Yes | Signature must match |
| Call private base virtual | ❌ No | Unless from inside base class |
| Dispatch through base | ✅ Yes | Works as long as base calls the virtual |
| Access private base virtual | ❌ No | Visibility rules apply during lookup |
Example: control Hierarchy with NVI Idiom
class control {
private:
virtual void paint() = 0; // Pure virtual, private
protected:
virtual void erase_background() {
std::cout << "erasing control background...\n";
}
public:
void draw() {
erase_background();
paint(); // Calls overridden paint() in derived class
}
virtual ~control() {} // Virtual to allow safe polymorphic deletion
};
class button : public control {
private:
void paint() override {
std::cout << "painting button...\n";
}
protected:
void erase_background() override {
control::erase_background();
std::cout << "erasing button background...\n";
}
};
class checkbox : public button {
private:
void paint() override {
std::cout << "painting checkbox...\n";
}
protected:
void erase_background() override {
button::erase_background();
std::cout << "erasing checkbox background...\n";
}
};
How It Works
draw()is the only public method responsible for drawing.paint()is private and must be implemented by derived classes.erase_background()is protected and optionally called by derived classes.- This structure allows:
- Enforced pre-/post-conditions in public methods.
- Derived classes to specialize behavior without altering public API.
- Clients to depend only on the guaranteed interface behavior.
Usage Example
std::vector<std::unique_ptr<control>> controls;
controls.emplace_back(std::make_unique<button>());
controls.emplace_back(std::make_unique<checkbox>());
for (auto& c : controls)
c->draw();
Minimal Overhead Example
class control {
protected:
virtual void initialize_impl() {
std::cout << "initializing control...\n";
}
public:
void initialize() {
initialize_impl(); // Public method calls protected virtual
}
};
class button : public control {
protected:
void initialize_impl() override {
control::initialize_impl();
std::cout << "initializing button...\n";
}
};
- Even though there's a level of indirection, compilers can optimize by inlining small functions.
Destructor Best Practices
Deleting a derived object through a base class pointer without a virtual destructor causes undefined behavior:
class Base {
// ❌ Not virtual!
~Base() { std::cout << "Base dtor\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived dtor\n"; }
};
Base* ptr = new Derived();
delete ptr; // 💥 UB! Only Base's dtor gets called
Suggestions:
| Design Pattern | Destructor Declaration | When to Use |
|---|---|---|
| 🔁 Polymorphic base | public: virtual ~Base(); |
Deleting via Base* is allowed/safe |
| 🔒 Non-polymorphic base | protected: ~Base(); |
Prevent deleting via base pointer |
| 🔐 Final class (no inheritance) | ~Class(); (default, non-virtual) |
No polymorphism; fast, compact |
✅ Design Case 1: Public and virtual — for polymorphic use
class Base {
public:
virtual ~Base() = default;
};
When to use:
- You intend the class to be used polymorphically (e.g., with base pointers).
- You want to allow users to write:
Base* b = new Derived(); delete b; - Ensures the derived class destructor gets called properly through the vtable.
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() = default; // ✅ allows deleting through Shape*
};
class Circle : public Shape {
public:
~Circle() { std::cout << "Circle dtor\n"; }
};
✅ Design Case 2: Protected and non-virtual — non-polymorphic, internal-use-only
class Base {
protected:
~Base() = default;
};
When to use:
- The class is not meant to be used polymorphically (i.e., not to be deleted through
Base*). - You don’t want clients to write
delete basePtr;— you restrict that by making the destructorprotected. - You don't need virtual dispatch because base destruction will always happen through
Derived. - Prevents external deletion:
delete basePtr;becomes a compile-time error. - Still allows
Derived(which inherits fromBase) to destroy the base part when deleted.
Example:
class RefCounted {
protected:
~RefCounted() = default; // ❌ cannot be deleted externally
};
class Resource : public RefCounted {
public:
~Resource() = default;
};
// Later:
RefCounted* rc = new Resource();
// delete rc; // ❌ Compile error: RefCounted::~RefCounted is protected
You’re saying: “Only I (or trusted code) should manage lifetime — not client code.”
Advantages of NVI Idiom
- Clear separation between interface and implementation.
- Enforces invariants and expected behavior.
- Safer class extension and specialization.
- Prevents public virtual methods from being misused by clients or mis-overridden in derived classes.
Handling friendship with the attorney-client idiom
- Problem with traditional friendship in C++
- Grants full access to all private and protected members.
- Breaks encapsulation, even if the friend only needs limited access.
- Creates tight coupling between classes.
Attorney-Client Idiom Overview
- Introduces an intermediary class (the "Attorney") to control access.
- The Client class makes only the Attorney its friend.
- The Attorney class grants access to specific private members via static functions.
- The actual friend accesses those specific parts through the attorney.
1. Initial Setup (Unrestricted Access)
class Client {
int data_1;
int data_2;
void action1() {}
void action2() {}
friend class Friend; // Full access (undesirable)
};
2. Use Attorney Class for Controlled Access
Client class only friends the attorney:
class Client {
int data_1;
int data_2;
void action1() {}
void action2() {}
friend class Attorney; // Restricted access via Attorney
};
Attorney class defines static accessors:
class Attorney {
static inline void run_action1(Client& c) { c.action1(); }
static inline int get_data1(Client& c) { return c.data_1; }
friend class Friend; // Only Friend can use these
};
Friend class uses attorney:
class Friend {
public:
void access_client_data(Client& c) {
Attorney::run_action1(c);
int d1 = Attorney::get_data1(c);
// No access to data_2 or action2()
}
};
How It Works
- Attorney is the only class that can access private members of
Client. - Friend is only allowed to call what Attorney explicitly exposes.
- Prevents misuse or accidental overreach into unrelated internals of
Client.
Polymorphism Example
class B {
virtual void execute() { std::cout << "base\n"; }
friend class BAttorney;
};
class D : public B {
void execute() override { std::cout << "derived\n"; }
};
class BAttorney {
static inline void execute(B& b) { b.execute(); }
friend class F;
};
class F {
public:
void run() {
B b;
D d;
BAttorney::execute(b); // prints: base
BAttorney::execute(d); // prints: derived
}
};
- Friendship is not inherited, but virtual dispatch still works.
BAttorney::execute(d)invokesD::execute()polymorphically.
Benefits
- Encapsulation is preserved.
- Fine-grained access control to private members.
- Can be tailored for different friends by creating multiple attorneys.
- Helps build extensible, secure APIs or frameworks.
Trade-offs
- More boilerplate code.
- Increases maintenance complexity.
- Can slow development/testing, especially in simpler applications.
When to Use
- When needing to expose only a subset of internals.
- For library/framework design where stable and restricted access is important.
- When different friends require different levels of access to the same class.
When not to use
If your app logic or business code relies on attorney-style access to internals — that’s a red flag. It means your public interface may be insufficient, or you need better design patterns like:
- Dependency injection
- Visitor/Strategy patterns
- Public façade over a private core
Static polymorphism with the curiously recurring template pattern
- Static vs. Runtime Polymorphism
- Runtime polymorphism (late binding): achieved via
virtualfunctions; resolved at runtime. - Static polymorphism (early binding): resolved at compile-time using function templates, operator overloading, or CRTP.
- Runtime polymorphism (late binding): achieved via
Curiously Recurring Template Pattern (CRTP)
- A derived class inherits from a base class template, with the derived class as the template parameter.
- Enables compile-time polymorphism by letting the base class call functions from the derived class.
template <class T>
class control {
public:
void draw() {
static_cast<T*>(this)->erase_background();
static_cast<T*>(this)->paint();
}
};
class button : public control<button> {
public:
void erase_background() {
std::cout << "erasing button background...\n";
}
void paint() {
std::cout << "painting button...\n";
}
};
class checkbox : public control<checkbox> {
public:
void erase_background() {
std::cout << "erasing checkbox background...\n";
}
void paint() {
std::cout << "painting checkbox...\n";
}
};
template <class T>
void draw_control(control<T>& c) {
c.draw();
}
button b;
draw_control(b);
checkbox c;
draw_control(c);
- CRTP delays instantiation until the derived class is fully defined.
static_cast<T*>(this)safely downcasts to the derived type inside the template.- Calls like
derived()->paint()are resolved at compile time.
Simplified CRTP with derived() helper
template <class T>
class control {
T* derived() { return static_cast<T*>(this); }
public:
void draw() {
derived()->erase_background();
derived()->paint();
}
};
Common Pitfalls with CRTP
-
Access control: functions called from the base must be
publicor base must be declared afriend.class button : public control<button> { private: friend class control<button>; void erase_background() { ... } void paint() { ... } }; -
No homogeneous containers: each instantiation (e.g.,
control<button>,control<checkbox>) is a distinct type.
Workaround for Homogeneous Storage
1. Abstract base class
class controlbase {
public:
virtual void draw() = 0;
virtual ~controlbase() {}
};
template <class T>
class control : public controlbase {
public:
void draw() override {
static_cast<T*>(this)->erase_background();
static_cast<T*>(this)->paint();
}
};
// No changes to `button` and `checkbox` needed
std::vector<std::unique_ptr<controlbase>> controls;
controls.emplace_back(std::make_unique<button>());
controls.emplace_back(std::make_unique<checkbox>());
for (auto& c : controls)
c->draw();
Advantages of CRTP
- No runtime cost: everything is resolved at compile time.
- Enables inline expansion of small functions.
- Simulates virtual function behavior without actual virtual dispatch.
Trade-offs
- Not intuitive at first glance.
- Cannot use base class pointers/references directly for polymorphic behavior (without a common abstract base).
- Template instantiation can increase binary size.
Adding functionality to classes with mixins
- Purpose of Mixins in C++
- Add reusable functionality to unrelated or already-defined classes.
- Enable composition through inheritance by injecting behavior without modifying the original class.
- Often confused with CRTP, but different in structure and direction of inheritance.
Key Difference: Mixin vs. CRTP
| Pattern | Structure | Behavior |
|---|---|---|
| CRTP | Derived : public Base<Derived> |
Base calls Derived’s functionality (compile-time polymorphism) |
| Mixin | Mixin<T> : public T |
Mixin adds functionality to base (T), typically unrelated |
Original classes with shared interface
class button {
public:
void erase_background() {
std::cout << "erasing button background...\n";
}
void paint() {
std::cout << "painting button...\n";
}
};
class checkbox {
public:
void erase_background() {
std::cout << "erasing checkbox background...\n";
}
void paint() {
std::cout << "painting checkbox...\n";
}
};
Mixin class adds new behavior
template <typename T>
class control : public T {
public:
void draw() {
T::erase_background();
T::paint();
}
};
so when you do control<button> - it creates something like
class control<button> : public button {
public:
void draw() {
button::erase_background(); // from button
button::paint(); // from button
}
};
Polymorphic Usage via Virtual Base
class control_base {
public:
virtual ~control_base() {}
virtual void draw() = 0;
};
Extend mixin to support runtime polymorphism
template <typename T>
class control : public control_base, public T {
public:
void draw() override {
T::erase_background();
T::paint();
}
};
Draw all controls
void draw_all(std::vector<control_base*> const& controls) {
for (auto& c : controls) {
c->draw();
}
}
int main() {
control<button> b;
control<checkbox> c;
std::vector<control_base*> controls = { &b, &c };
draw_all(controls);
}
How It Works
- control
inherits from both control_base(for polymorphism) andT(to reuse functionality). - New behavior (
draw()) is implemented using existing methods fromT. - Compile-time reuse + runtime dispatch (if base interface is added).
When to Use Mixins
- You want to extend functionality without altering the original class.
- Multiple unrelated types need a shared behavior (e.g., logging, serialization, drawing).
- You need reusable behavior blocks composed via inheritance.
Limitations
- Not naturally polymorphic — need a virtual base like
control_base. - Potential for diamond inheritance or ambiguity if multiple mixins are used carelessly.
- Template code increases compilation time and binary size due to duplication.
Comparison Summary
| Feature | CRTP | Mixin |
|---|---|---|
| Inheritance direction | Derived : Base<Derived> |
Mixin<T> : T |
| Purpose | Inject derived behavior into base | Add common behavior to existing |
| Virtual dispatch needed | No (static polymorphism) | Optional (via virtual base) |
| Used for | Compile-time customization, policy | Functionality reuse, composition |
| Homogeneous containers | Requires workaround (virtual base) | Works with added virtual base |
Expand a bit: mix in multiple base classes into a single control
using variadic templates and recursive inheritance
template <typename... Components>
class control;
We’ll recursively inherit from each component:
// Recursive class
template <typename Head, typename... Tail>
class control<Head, Tail...> : public Head, public control<Tail...> {
public:
void draw() {
Head::erase_background();
Head::paint();
control<Tail...>::draw(); // recurse
}
};
// Base case (when there's only one type)
template <typename Last>
class control<Last> : public Last {
public:
void draw() {
Last::erase_background();
Last::paint();
}
};
// then you can call
control<button, checkbox> multi;
multi.draw();
Note, for control<Tail...>::draw(); - It's just calling the draw() function on the next "layer" of inheritance
- namely, a control that wraps the remaining types in the variadic pack.
So:
control<T1, T2, T3>::draw() calls T1::methods() and then control<T2, T3>::draw()
⚠️ Caveats
-
Diamond Inheritance?
If two components inherit from the same base — beware. You may needvirtualinheritance to avoid ambiguity. -
Name conflicts?
If multiple components have the same method name but different semantics (e.g. both definereset()), you'll get ambiguity.You may resolve this with:
using Head::erase_background; using Head::paint;Or disambiguate explicitly:
Head::erase_background(); control<Tail...>::draw(); -
Complex construction
If any component has non-default constructors, you’ll need to pass and forward parameters properly.
Handling unrelated types generically with the type erasure idiom
Why Do We Want Type Erasure?
Because C++'s regular polymorphism (via virtual functions) only works when:
- All types inherit from a common base class (class hierarchy)
- You own or can modify the types
- You are okay with the tight coupling that comes with that hierarchy
But what if:
- The types come from different libraries?
- They don’t inherit a common base?
- You want to handle lambdas, functors, pointers, objects uniformly?
- You want to store different types in a single container?
🎯 Type erasure allows you to treat unrelated types polymorphically
without modifying them, without inheritance, and without knowing their type.
A Simple Analogy: std::function, you write:
std::function<int(int)> f;
f = [](int x) { return x * 2; };
f = std::bind(&SomeClass::method, obj, std::placeholders::_1);
f = my_callable_object;
All these types are different, but std::function erases their types and gives you a uniform way to call them.
You don’t care about the actual type — just that it’s callable with an int and returns an int.
Duck Typing vs. Type Erasure
-
Duck typing: If it walks like a duck… operate based on behavior, not type.
-
Type erasure: Wraps any type that satisfies a behavioral interface into a uniform type.
- Removes (erases) concrete type info.
- Enables runtime polymorphism for unrelated types.
Given types
class button {
public:
void erase_background() { std::cout << "erasing button background...\n"; }
void paint() { std::cout << "painting button...\n"; }
};
class checkbox {
public:
void erase_background() { std::cout << "erasing checkbox background...\n"; }
void paint() { std::cout << "painting checkbox...\n"; }
};
Type-erased wrapper: control
struct control {
template <typename T>
control(T&& obj)
: ctrl(std::make_shared<control_model<T>>(std::forward<T>(obj))) {}
void draw() { ctrl->draw(); }
struct control_concept {
virtual ~control_concept() = default;
virtual void draw() = 0;
};
template <typename T>
struct control_model : control_concept {
control_model(T& unit) : t(unit) {}
void draw() override {
t.erase_background();
t.paint();
}
private:
T& t;
};
private:
std::shared_ptr<control_concept> ctrl;
};
int main() {
checkbox cb;
button btn;
std::vector<control> controls = { control(cb), control(btn) };
for (auto& c : controls)
c.draw();
}
How it works
control_model<T>stores a reference to the object and forwards calls.control_conceptis the type-erased interface (pure virtual).controlhides implementation behind a shared pointer tocontrol_concept.- Enables runtime polymorphism without requiring inheritance in the original types.
Alternative: Explicit Wrapper Hierarchy (More Verbose)
struct control_concept {
virtual ~control_concept() = default;
virtual void draw() = 0;
};
template <typename T>
struct control_wrapper : control_concept {
control_wrapper(T& obj) : t(obj) {}
void draw() override {
t.erase_background();
t.paint();
}
private:
T& t;
};
Usage
checkbox cb;
button btn;
control_wrapper<checkbox> cbw(cb);
control_wrapper<button> btnw(btn);
std::vector<control_concept*> v = { &cbw, &btnw };
for (auto& c : v)
c->draw();
- Redundant code avoided using a class template (
control_wrapper). - Still requires manual management of pointers.
Encapsulating a Collection
struct control_collection {
template <typename T>
void add_control(T&& obj) {
ctrls.push_back(std::make_shared<control_model<T>>(std::forward<T>(obj)));
}
void draw() {
for (auto& c : ctrls)
c->draw();
}
struct control_concept {
virtual ~control_concept() = default;
virtual void draw() = 0;
};
template <typename T>
struct control_model : control_concept {
control_model(T& unit) : t(unit) {}
void draw() override {
t.erase_background();
t.paint();
}
private:
T& t;
};
private:
std::vector<std::shared_ptr<control_concept>> ctrls;
};
Usage
int main() {
checkbox cb;
button btn;
control_collection cc;
cc.add_control(cb);
cc.add_control(btn);
cc.draw();
}
Advantages of Type Erasure
- Enables runtime polymorphism for unrelated types.
- Decouples interfaces from inheritance.
- Type-safe alternative to
void*. - Extensible without modifying original types.
C++ Standard Library Usage
std::function: wraps any callable type.std::any: holds any copyable value.std::shared_ptr: type-erased deleter.
Comparison with Other Idioms
| Idiom | Purpose | Base Required | Polymorphism Type |
|---|---|---|---|
| CRTP | Compile-time polymorphism | Yes | Static |
| Mixins | Extend functionality via templates | No | Static |
| Type Erasure | Runtime polymorphism for unrelated types | No | Dynamic |
Caution
- Overhead from dynamic allocation (
std::shared_ptr). - May increase code complexity.
- If performance critical, prefer static polymorphism (CRTP).
Beautiful — you're asking the big-picture question now:
“What is type erasure really about? Why all these layers of abstraction?”
Let’s walk through the what, why, and how of type erasure — step by step — and then zoom in on the purpose behind each abstraction level.
🧠 What is Type Erasure?
Type erasure is a design technique where you:
Hide (or erase) the concrete type information of an object at compile time,
while still allowing certain operations to be performed on it at runtime.
Instead of saying:
“I know exactly what type this object is”
you say:
“I don’t care what type it is, as long as it has certain behavior.”
What's Actually Happening Internally?
1. The Concept (interface)
struct concept {
virtual void do_something() = 0;
virtual ~concept() = default;
};
This abstract base class serves as the runtime interface for the erased types.
It answers this question:
Once I've erased the type... how do I still know what I can do with it?
Since we erased the concrete type, we can’t access its real members anymore — we need a uniform interface to tell us what operations are allowed.
So concept is the “contract” — it says:
🗣️ “I don’t care what exact type you are. As long as you can implement do_something(), you can sit in this container and be operated on.”
🧱 Why not just store the object directly?
You can't — because in type erasure, you're intentionally hiding the object's type behind a pointer to a base class (concept* or smart pointer). This means:
- You don't know the type at compile time anymore
- So you can't directly call
obj.do_something()unless you define what it means in some shared interface
Hence, the concept interface is what lets you interact with the object after the type has been erased.
This means:
- We don’t need all types to share a common base class.
- We just need them to support certain operations (like
paint()anderase_background()).
The concept class is where we codify the duck:
“Anything that can
do_something()is duck enough for me.”
👇 What happens without concept?
If you erase the type (say, using void* or std::any) without a virtual interface, then:
- You can't call any method on the object
- You must cast back to the original type — which defeats the whole purpose of type erasure
- It becomes unsafe and unstructured
So concept gives you a safe, structured, extensible interface to interact with any wrapped type, without knowing its type.
E.g. It defines what all "ducks" must do, and the model<T> classes adapt specific types into that duck form.
2. The Model (wrapper)
template <typename T>
struct model : concept {
T obj;
model(T x) : obj(std::move(x)) {}
void do_something() override {
obj.do_something(); // or obj(), obj.paint(), etc.
}
};
📌 This wraps the concrete type. It tells the concept:
"I know how to use this specific type to implement the expected behavior."
This is the glue between the erased interface and the real object.
3. The Erased Type (handle class)
class erased_type {
std::unique_ptr<concept> ptr;
public:
template <typename T>
erased_type(T x) : ptr(std::make_unique<model<T>>(std::move(x))) {}
void do_something() {
ptr->do_something();
}
};
📌 This is the unifying interface. It exposes only the abstract operations.
Clients don’t need to know (or care) what the real type is — they just use do_something().
Layers of abstractions
| Layer | Responsibility |
|---|---|
| Concept | Defines the interface / expected behavior |
| Model | Bridges concrete types to the concept |
| Handle | Hides the details and provides a clean interface |
You decouple the type from its behavior, and in return:
- Get runtime polymorphism for arbitrary types
- Maintain type safety (unlike
void*) - Avoid inheritance requirements
Implementing a thread-safe singleton
- Purpose of the Singleton Pattern
- Restricts a class to a single instance.
- Provides a global access point to that instance.
- Commonly used for shared resources (e.g., config managers, loggers).
- Thread-safety is essential when accessed from multiple threads.
Basic Singleton Implementation
class Singleton {
private:
Singleton() = default;
public:
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton& instance() {
static Singleton single; // Initialized once, thread-safe since C++11
return single;
}
};
- Why it's thread-safe:
- C++11 guarantees that
staticlocal variables are initialized exactly once, even in multithreaded contexts. - First call to
instance()triggers initialization. - Subsequent calls return the already-initialized object.
- C++11 guarantees that
Quote from C++ Standard (N4917, §6.7.5.2)
“If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.”
Why Return a Reference?
instance()returns a reference (Singleton&) because:- There’s no scenario where it would return null.
- Prevents accidental deletion (as might be the case with a pointer).
Generic Singleton with CRTP
1. Singleton Base Template
template <class T>
class SingletonBase {
protected:
SingletonBase() = default;
public:
SingletonBase(SingletonBase const&) = delete;
SingletonBase& operator=(SingletonBase const&) = delete;
static T& instance() {
static T single;
return single;
}
};
```cpp
class Single : public SingletonBase<Single> {
private:
Single() = default;
friend class SingletonBase<Single>;
public:
void demo() {
std::cout << "demo\n";
}
};
int main() {
Single::instance().demo(); // prints "demo"
}
- CRTP Usage:
- Avoids code duplication across multiple singleton types.
SingletonBase<T>manages the instantiation.T(e.g.,Single) provides the concrete type.
Design Considerations
- Singleton usage can lead to hidden dependencies and global state, which may:
- Complicate testing (no easy way to reset or replace instance).
- Break encapsulation and violate SRP (Single Responsibility Principle).
- Use when:
- Only one instance is logically valid.
- Shared, read-only access is required.
Alternatives to Singleton
- Dependency Injection (pass shared instance explicitly).
- Static class members.
- Scoped or thread-local storage (when isolation is needed).