The ‘Invisible’ Philosophy of C++ Design Patterns: Beyond Singleton, Where Else Do We Use Them?

The 'Invisible' Philosophy of C++ Design Patterns: Beyond Singleton, Where Else Do We Use Them?The 'Invisible' Philosophy of C++ Design Patterns: Beyond Singleton, Where Else Do We Use Them?Click the blue text to follow the author

1. The ‘Invisible’ Mystery of C++ Design Patterns

Have you ever thought: apart from the ubiquitous Singleton pattern in daily C++ code, the ‘standard’ implementations of other patterns rarely appear.

1.1. Why Do We Only See Singleton?

Design patterns, especially the 23 classic patterns proposed by the Gang of Four (GoF), provide reusable solutions to common design problems. However, the frequency of design pattern usage in C++ is somewhat different:

  1. Many people learn design patterns through examples based on Java or C#, which require a lot of interfaces and abstract classes. If these design patterns are directly applied to C++ projects, the code becomes very lengthy and complex, reducing readability and efficiency.
  2. The Singleton pattern, due to its simple concept and direct implementation, has a clear demand in scenarios like global configuration and logging systems, making it the most commonly seen and used pattern in C++ codebases. This creates an illusion: if a pattern is not as prominent as Singleton, it seems to not exist.

This is because we view design patterns as a fixed structure, rather than a design philosophy.

1.2. The ‘Dimensionality Reduction’ of C++ Language Features

The birth of design patterns largely aimed to compensate for the limitations of early object-oriented languages in expressiveness or to solve certain language constraints in memory management and behavioral abstraction. However, C++, especially modern C++ (C++11 and later), has some unique and powerful language features that can achieve the core goals of certain patterns in a more concise and safer way, making traditional pattern implementations redundant.

Take the Proxy pattern as an example. To manage resources securely or control access, one would typically need to manually create a proxy class to wrap the actual object. But in C++:

  • Smart pointers: <span>std::unique_ptr</span> and <span>std::shared_ptr</span> are themselves resource management proxies. Through the RAII mechanism, they provide memory management proxies at compile time and runtime, eliminating the need to manually write complex proxy class structures.
  • Resource Acquisition Is Initialization (RAII): This C++-specific mechanism manages resources through the lifecycle of objects, fundamentally solving many resource release and state transition issues that would otherwise require the Facade pattern or State pattern.

The features of C++ such as templates, generic programming, lambda expressions, and <span>std::function</span> enhance the language’s abstraction capabilities, making many behavioral patterns no longer reliant on traditional, virtual function-based polymorphism.

  • Simplification of the Strategy pattern: The Strategy pattern typically requires defining an abstract base class and multiple derived classes. Modern C++ can directly use <span>std::function</span> to store and switch between different algorithms (i.e., strategies), where the algorithms themselves can be lambda expressions, function pointers, or functors. This approach avoids creating numerous class definition files, making the code more inline and efficient.
  • Template metaprogramming: Templates and compile-time polymorphism can achieve more efficient structural extensions than runtime polymorphism. They can elegantly implement the Decorator pattern or Bridge pattern, pushing the implementation of design patterns to compile time, making them ‘invisible’ at runtime.

Thus, the true meaning of ‘design patterns are used less in C++’ is: the heavyweight implementations of GoF patterns are rarely needed because the C++ language itself provides lighter and more efficient alternatives to achieve the design intentions behind the patterns. This is the ‘invisibility’ of C++ design patterns.

2. How Language Features ‘Consume’ Patterns

The value of design patterns lies in providing universal, reusable structures to solve common problems in software design. However, when a language itself offers powerful mechanisms to address these issues, the complex structures of traditional patterns will naturally be ‘consumed’ or ‘simplified’. The features of modern C++ (C++11/14/17/20/23) do just that, integrating the intentions of patterns into the idioms of the language.

The 'Invisible' Philosophy of C++ Design Patterns: Beyond Singleton, Where Else Do We Use Them?

2.1. RAII Mechanism and Resource Management

RAII (Resource Acquisition Is Initialization) is the cornerstone of C++, managing resources through the lifecycle of objects. Resources are acquired when an object is created, and released automatically through the destructor when the object is destroyed (whether through normal exit or exception throw).

The Proxy pattern or Facade pattern is mainly used to encapsulate complex resource operations or lifecycle management. However, C++’s RAII mechanism guarantees resource safety at the language level, simplifying the need for these patterns.

  • Smart pointers are perfect resource proxies. They encapsulate raw pointers and automatically call <span>delete</span> upon destruction. This achieves the goals of ‘controlling access’ and ‘resource management’ in the Proxy pattern without manually writing proxy classes.
  • Lock guards (<span>std::lock_guard</span>): <span>std::lock_guard</span> locks a mutex upon construction and automatically unlocks it upon destruction. This solves the complexity of resource access in concurrent scenarios, implementing the Facade pattern (encapsulating complex locking/unlocking processes) and the Proxy pattern (controlling access to shared resources) in a simple, exception-safe manner.

The RAII mechanism integrates the safety and automation of resource management into the language itself, making heavyweight proxies or facade patterns unnecessary in C++.

2.2. Abstraction of Lambda and Function

Object-oriented design requires relying on virtual functions and inheritance to achieve dynamic switching or passing of behaviors, corresponding to the Strategy pattern and Command pattern. Modern C++ function objects, lambda expressions, and <span>std::function</span> greatly simplify this process.

The core of the Strategy pattern is to define a series of algorithms and encapsulate them so that they can be interchanged.

  • Traditional implementation: Requires defining an <span>IStrategy</span> interface, then defining a concrete class for each algorithm that inherits from <span>IStrategy</span>.
  • Modern C++ implementation: Can use <span>std::function<ReturnType(Args...)></span> to hold any callable object.

Example:

// Traditional pattern requires: 
// class Comparator { public: virtual bool compare(int a, int b) = 0; }; 
// class AscendingComparator : public Comparator { ... }; 

// Modern C++ implementation:
#include <functional>
#include <algorithm>
#include <vector>

void sort_data(std::vector<int>& data, std::function<bool(int, int)> strategy) {
    std::sort(data.begin(), data.end(), strategy);
}

// Usage:
// Ascending strategy (Lambda expression)
sort_data(my_vec, [](int a, int b){ return a < b; }); 
// Descending strategy (std::greater)
sort_data(my_vec, std::greater<int>()); 

Modern C++ achieves the goal of the Strategy pattern—runtime behavior switching—without creating any additional classes or complex inheritance structures, significantly reducing template code.

The Command pattern encapsulates an operation request as an object, allowing for parameterization, queuing, logging, and supporting undo/redo operations.

  • Traditional implementation: Requires defining an <span>ICommand</span> interface and multiple concrete command classes.
  • Modern C++ implementation:<span>std::function<void()></span> can directly encapsulate a command with no parameters. To support undo, a simple struct containing two <span>std::function</span> (<span>execute</span> and <span>undo</span>) can be defined.

2.3. Templates and Generic Programming

The template mechanism in C++ provides powerful compile-time polymorphism, which can sometimes be more flexible and performant than runtime polymorphism (virtual functions), replacing certain structural patterns.

The Bridge pattern and Decorator pattern are mainly used to separate abstraction from implementation or dynamically add responsibilities to objects.

  • Templates as a bridge: C++ uses template parameters to ‘bridge’ different implementation details. For example, a container class can decide its memory allocation strategy through template parameters, without needing to use runtime virtual function calls.
  • CRTP: The Curiously Recurring Template Pattern (CRTP) allows derived classes to inherit from a base class that uses itself as a template parameter. This enables the base class to access members of the derived class at compile time, achieving static polymorphism or functionality injection.

Function injection (Mixin) and decoration: Through templates and CRTP, static decorators or Mixins can be implemented, injecting additional functionality into classes at compile time, avoiding the extra object hierarchy and virtual function overhead introduced by the Decorator pattern at runtime.

The language features of modern C++, especially RAII, function objects, and templates, provide more direct and efficient ‘shortcuts’ to implement design patterns. There is no longer a need to rely on the classic structures of GoF patterns; instead, the ideas of patterns are integrated into daily idioms and standard libraries, achieving the ‘invisibility’ of patterns.

3. Built-in Patterns in Frameworks and Standard Libraries

Design patterns do not only exist in the business logic code that we write, but also in the C++ ecosystem. We are not ‘implementing’ patterns; we are ‘using’ patterns, as they are already encapsulated in the Standard Template Library (STL) and the underlying architectures of various mainstream frameworks.

3.1. Textbook Application of the Iterator Pattern

The Standard Template Library (STL) is a paradigm of C++ generic programming, decoupling the concepts of containers, algorithms, and iterators, and this decoupling is achieved through design patterns.

The Iterator pattern provides a way to sequentially access the elements of an aggregate object without exposing its internal representation.

  • Embodiment in STL:<span>std::vector::iterator</span>, <span>std::map::iterator</span>, <span>std::list::iterator</span>, etc.
  • Value: Regardless of whether the underlying data structure is contiguous memory or node-based, they can be traversed using a unified interface (<span>++</span>, <span>*</span>, <span>!=</span>) to achieve the goal of the Iterator pattern: separating traversal algorithms from container structures.
  • Modern usage: The range-based <span>for</span> loop introduced in C++11 (<span>for (auto& item : container)</span>) has simplified the use of the Iterator pattern to the extreme, making developers almost unaware of the existence of iterators, while the underlying mechanism remains the Iterator pattern.

The Adapter pattern converts the interface of one class into another interface that clients expect.

  • Embodiment in STL:Container adapters, such as <span>std::stack</span>, <span>std::queue</span>, and <span>std::priority_queue</span>.
  • Value: They are not new containers but provide specific behaviors by composing existing containers (defaulting to <span>std::deque</span> or <span>std::vector</span>) and restricting their interfaces.
  • <span>std::stack</span> adapts the underlying container, exposing only <span>push</span>, <span>pop</span>, and <span>top</span> interfaces, implementing last-in-first-out (LIFO) behavior.
  • <span>std::queue</span> adapts the underlying container, exposing only <span>push</span>, <span>pop</span>, and <span>front</span> interfaces, implementing first-in-first-out (FIFO) behavior.

3.2. GUI Frameworks

One of the most popular GUI frameworks in C++ is Qt, which elegantly implements core patterns.

The Observer pattern defines a one-to-many dependency between objects, so that when one object’s state changes, all dependent objects are notified and updated automatically.

  • Embodiment in Qt:Signals & Slots mechanism.
  • Value: Qt’s Signals & Slots is the most famous implementation of the Observer pattern in the C++ ecosystem. Signal corresponds to the ‘subject’ of the Observer pattern. Slot corresponds to the ‘observer’ of the Observer pattern. Connections are established through the <span>connect()</span> function, enabling type-safe, loosely-coupled communication. When an object emits a signal, all slot functions connected to that signal are automatically called, without the signal emitter needing to know or depend on any specific receiver classes.

The Mediator pattern encapsulates a series of object interactions through a mediator object. The Mediator pattern allows objects to interact without explicitly referencing each other, thus reducing coupling.

  • Embodiment in Qt: A parent component in dialog or main window design acts as a mediator, responsible for receiving signals from child components and coordinating responses from other child components.
  • Value: This avoids complex web-like dependencies between components, enhancing modularity and maintainability in large GUI applications.

3.3. Patterns in Compilers and Interpreters

Even in low-level systems and toolchains, patterns are ubiquitous: the Visitor pattern.

The Visitor pattern represents an operation on the elements of an object structure. It allows defining new operations on these elements without changing their classes.

  • Application scenario:Traversal of the abstract syntax tree (AST) in compilers and interpreters.
  • Value: When a compiler needs to perform multiple operations on an AST, the Visitor pattern can avoid continuously adding new methods to the core AST node classes, thus adhering to the Open/Closed Principle (open for extension, closed for modification).

Therefore, C++ does not lack design patterns; rather, these patterns have been abstracted and encapsulated, becoming part of the tools and frameworks we use daily. Standing on the shoulders of giants, we enjoy the architectural advantages brought by design patterns without having to re-implement their underlying details.

4. The Five Indispensable Design Patterns

Modern C++ language features can replace some lightweight patterns, but in large, complex, and highly variable system architectures, we still rely on classic GoF design patterns to provide structured solutions.

The 'Invisible' Philosophy of C++ Design Patterns: Beyond Singleton, Where Else Do We Use Them?

4.1. Abstract Factory / Factory Method

Problem solved: Encapsulates the object creation process, allowing the system to create objects without specifying concrete classes. Implements ‘programming to an interface’ and ‘dependency inversion principle’.

The factory pattern in C++ is most commonly used to handle platform differences and pluggability.

  1. Cross-platform component creation: Software needs to run on both Windows and Linux, creating platform-specific window objects and file system objects. The Abstract Factory can define a <span>GUIFactory</span> interface, containing <span>createWindow()</span> and <span>createFS()</span> methods. At runtime, the system loads the specific implementation of <span>WindowsFactory</span> or <span>LinuxFactory</span> based on the operating system, ensuring that the business logic code only depends on the abstract interface, decoupling it from the specific platform implementation.
  2. Plugin system/module loading: To dynamically load DLLs or SO files, these external modules must provide a standard interface for creating objects, and the factory method is the only choice. The program only knows the factory interface, not the specific implementation details of the plugins.

4.2. Observer Pattern

Problem solved: Strictly decouples publishers and subscribers, implementing a one-to-many dependency relationship, ensuring that state changes notify all relevant parties.

Qt’s Signals & Slots is an excellent implementation of the Observer pattern, but in a pure C++ environment without Qt, it still needs to be manually implemented or use the Boost Signal library.

  1. Data model-view synchronization: Any architecture that separates data from multiple display interfaces requires the Observer pattern. When data changes, the Model acts as the subject, notifying all registered Views to update themselves, while the Model itself does not need to know how many Views there are or how these Views render the data.
  2. Logging and monitoring systems: Core business modules generate log events, and different log processors subscribe to these events, achieving flexible log distribution.

4.3. Command Pattern

Problem solved: Encapsulates an operation request as an object, allowing for parameterization, queuing, logging, and supporting undo/redo operations.

The Command pattern is irreplaceable in complex applications that require history tracking and rollback functionality.

  1. Graphic editor or text editor: Every user operation is encapsulated as a <span>Command</span> object. The <span>execute()</span> method performs the operation. The <span>unexecute()</span> method performs the inverse operation. By maintaining a command stack, unlimited undo and redo functionality can be achieved.
  2. Transaction management: Encapsulates a series of database operations into a command object. If an error occurs during execution, the <span>unexecute()</span> or specific rollback logic can be called to ensure data consistency.

4.4. Adapter Pattern

Problem solved: Resolves interface incompatibility, allowing classes that cannot work together to collaborate.

The Adapter pattern is ‘glue code’ for handling external dependencies and legacy issues.

  1. Integrating legacy C APIs: C++ often needs to call numerous C language libraries. These C libraries use structures and function pointers. An object adapter can encapsulate these C functions, converting them into interfaces with modern C++ style (classes, member functions, exception safety) for use in other parts of the project.
  2. Unified database drivers: Applications need to support MySQL, PostgreSQL, and SQLite. Although they have similar functionalities, their APIs differ. A unified <span>DBConnection</span> interface can be defined, and an adapter class can be implemented for each database, converting unified interface calls into specific database driver function calls.

4.5. Decorator Pattern

Problem solved: Dynamically adds additional responsibilities to an object. Compared to using inheritance to extend functionality, decorators provide greater flexibility.

The Decorator pattern is very useful in scenarios where functionality needs to be flexibly combined, and the combination method is uncertain.

I/O stream processing: A basic file writer (<span>FileWriter</span>). To dynamically add functionality:

  • <span>CompressionDecorator</span>: Compress data before writing.
  • <span>EncryptionDecorator</span>: Encrypt data after compression.
  • <span>BufferedDecorator</span>: Add buffering mechanism to improve performance.
  • Decorators can combine these functionalities freely without creating new subclasses for each combination.

Network protocol stack: When processing network packets, each decorator can represent a layer in the protocol stack, dynamically adding or stripping functionalities from the original packet.

These five patterns (Factory, Observer, Command, Adapter, Decorator) are essential tools that C++ architects must master and explicitly apply to design complex, scalable, and loosely-coupled systems. They go beyond the simple alternatives provided by language features, addressing organizational and interaction issues at the macro architectural level.

5. Conclusion

The value of design patterns lies not in the formal class diagrams and inheritance structures, but in the design philosophies behind them.

The use of C++ design patterns: intent over form. The pursuit is for the decoupling, scalability, and maintainability brought by patterns, rather than strictly adhering to the class diagrams in the GoF book.

Pattern Intent Traditional Form (GoF) Modern C++ Idioms
Resource Management Proxy Pattern RAII Mechanism, Smart Pointers (<span>std::unique_ptr</span>)
Behavior Switching Strategy Pattern <span>std::function</span>, Lambda Expressions
Traversal Abstraction Iterator Pattern STL Iterators, Range-based for loop
Function Injection Decorator Pattern Templates, CRTP, Mixin

With the popularization of new features like C++20 Modules and Concepts, the abstraction capabilities of C++ will further enhance.

  • Concepts: Generic programming becomes safer and easier, simplifying the complex pattern implementations of template metaprogramming, making template-based static patterns more mainstream.
  • Automation of patterns: Many modern tools and frameworks are further automating the implementation of design patterns.

The status of design patterns in C++ will not diminish; it will only become more mature and subtle. From prominent class hierarchies, they will gradually integrate into the idioms of the language, the interfaces of the standard library, and the underlying architectures of frameworks.

Looking back at past tutorials

Recommended: 6 C++ open-source projects suitable for practice, so you won’t just be talking theory!

C++

STL Algorithms: Master these, and your code efficiency will improve tenfold!

Building

Using Google Test for C++ Unit Testing

Applications

Thoroughly understand asynchronous and multithreading: concepts, principles, and application scenario comparisons

The 'Invisible' Philosophy of C++ Design Patterns: Beyond Singleton, Where Else Do We Use Them?Lion Welcome to follow my public account for learning technology or submissions

Leave a Comment