24 minutes
Implementing Common Design Patterns in C++17
Introduction
Design patterns are proven solutions to recurring design problems in software development. They provide templates for structuring code in ways that improve flexibility, extensibility, and maintainability. In this post, I will explore four classic design patterns – Singleton, Factory Method, Observer, and Visitor – and demonstrate how to implement them using modern C++17 features. These patterns were originally popularised by the “Gang of Four” (GoF) book in the 1990s, and while some argue that modern C++ features have reduced the need for certain patterns, a solid understanding of them remains highly valuable. Each section below introduces a pattern, provides a C++17 code snippet, and discusses its use cases, benefits, and potential drawbacks (with a bit of my own perspective as an experienced C++ developer).
Singleton Pattern
The Singleton is a creational design pattern that ensures only one instance of a class exists and provides a global access point to that instance. In practice, a Singleton class has a private constructor and a static method (often named getInstance) that creates or returns the sole instance. This pattern is useful for representing unique resources like configuration managers or hardware interfaces. However, it must be used judiciously – singletons effectively act as global variables, which can introduce hidden dependencies and complicate testing and modularity. In fact, many C++ developers consider Singleton an anti-pattern, and its usage has declined in modern C++ code. I admit that in my early years I found Singletons convenient, but over time I’ve grown cautious with them due to these pitfalls.
A simple thread-safe implementation in C++17 can leverage “Meyers’ Singleton”, which uses a local static variable inside the getInstance() function. Since C++11, the initialization of function-local static variables is guaranteed to occur exactly once in a thread-safe manner. This means we avoid explicit locks and still safely initialize the singleton even in multithreaded scenarios. Here’s how we can implement a Singleton in C++17:
#include <memory>
class Singleton {
public: // Deleted copy constructor and assignment to prevent copies. Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// Global point of access: static method to get the single instance. static Singleton& getInstance() {
static Singleton instance;
// Lazily initialized on first call, thread-safe (C++11 guarantee). return instance;
} // Example business logic method void doSomething() {
// ... (some functionality)
}
private: Singleton() = default;
// Private constructor ~Singleton() = default;
// (Optional) private destructor
};
In this code, Singleton::getInstance() returns a reference to a static local instance. The first call initializes it, and subsequent calls return the same object. C++ ensures this initialization happens only once, even if multiple threads call getInstance() concurrently. We delete the copy constructor and assignment operator to prevent creating additional instances (ensuring a single instance). To use the Singleton, one would simply call Singleton::getInstance().doSomething() anywhere in the program.
It’s worth noting a couple of C++17 specifics here. First, C++17 introduced inline static variables, which allow us to define static class members inside the class definition itself. We could, for example, define an inline static std::unique_ptr<Singleton> instance in the class and use it for the singleton object. This eliminates the need for a separate out-of-class definition of the static member. However, such an approach still requires careful handling for thread safety and the static initialization order fiasco (a situation where the initialization order of static objects across translation units is undefined). Using the function-local static as above bypasses the initialization order problem by constructing the object on first use (after program start) and is inherently thread-safe, which is why I favor it.
Pros: The Singleton ensures a single shared resource is easily accessible, and it can simplify access to global services or configurations. The implementation shown is straightforward in modern C++ and incurs minimal overhead for the first access (subsequent accesses are just a reference return, plus a trivial boolean check due to thread-safe static init).
Cons: Singleton can encourage global state. As a result, it can harm modular design and make unit testing difficult, since any code using the Singleton is hard-coded to that single instance. In my experience, excessive use of Singletons leads to code that is tightly coupled and tricky to maintain. Many experts advise limiting Singletons or using dependency injection and explicit wiring of components instead. If a Singleton is truly needed, one should document its usage and ensure it doesn’t become a hidden labyrinth of state shared across the codebase.
(Personal perspective: I use Singletons very sparingly. When I do, I often combine them with other patterns – for example, a Singleton that is also a Factory – to keep design clean. I always ask myself if a singleton is truly necessary or if a well-managed global object (or function parameter) could suffice.)
Factory Method Pattern
The Factory Method is another creational pattern, aimed at abstracting the object creation process. It “provides an interface for creating an object, but lets subclasses alter the type of object that will be created” (GoF). In simpler terms, a Factory Method helps construct product objects without specifying their exact concrete classes in the code that uses them. This is useful when a code section (the client) needs to work with abstract interfaces, and the decision of which concrete class to instantiate can be delegated to a factory. By using a factory, we encapsulate the creation logic in one place, making it easier to extend and modify. The pattern is widely used in C++ code to provide flexibility in evolving designs.
There are a few variations of the factory concept in C++. The factory method pattern, in its classic form, usually involves a base “Creator” class with a virtual factory method that derived classes override to instantiate different products. Another approach is the simple factory (or static factory function), where a standalone function or static method creates objects based on input parameters. Here, I’ll demonstrate a simple factory function in modern C++17, which is often sufficient for many use cases and avoids some boilerplate.
Suppose we have an abstract interface Shape with concrete implementations like Circle and Square. We want a factory that can produce the right Shape object based on a parameter (for example, an enum or string). Using modern C++ smart pointers is prudent to manage memory, so our factory will return a std::unique_ptr<Shape> – a smart pointer owning the created object. (A std::unique_ptr automatically deletes the object when it goes out of scope, preventing memory leaks.)
#include <memory>
#include <string>
#include <iostream> // Product interface
class Shape {
public: virtual ~Shape() = default;
virtual std::string draw() const = 0;
// Pure virtual function
};
// Concrete products
class Circle : public Shape {
public: std::string draw() const override {
return "Drawing a Circle";
}
};
class Square : public Shape {
public: std::string draw() const override {
return "Drawing a Square";
}
};
// Factory function to create shapes enum
class ShapeType {
CircleType, SquareType
};
std::unique_ptr<Shape> createShape(ShapeType type) {
switch (type) {
case ShapeType::CircleType: return std::make_unique<Circle>();
// C++14/17 make_unique, returns unique_ptr<Circle> case ShapeType::SquareType: return std::make_unique<Square>();
default: throw std::invalid_argument("Unknown ShapeType");
}
}
int main() {
auto shape1 = createShape(ShapeType::CircleType);
std::cout << shape1->draw() << std::endl;
// Outputs: Drawing a Circle auto shape2 = createShape(ShapeType::SquareType);
std::cout << shape2->draw() << std::endl;
// Outputs: Drawing a Square return 0;
}
In the createShape factory, we use an enum class ShapeType to decide which shape to create. The function uses std::make_unique (introduced in C++14) to construct a Circle or Square and return it as a std::unique_ptr<Shape> (automatically upcasting to the base class pointer type). The caller of createShape doesn’t need to know about the Circle or Square classes’ constructors – they simply request a shape by type. This decouples the object creation from usage. If later we add new shape types (say, Triangle), we can extend the factory without changing the client code that uses Shape objects.
The benefit of this pattern is clearer when dealing with more complex creation logic or families of related products. For example, consider a GUI toolkit where Shape could be replaced with an abstract Button class, and we have different concrete Button subclasses for different platforms (Windows, macOS, etc.). A factory method could create the appropriate Button subclass depending on the current OS, without the higher-level code needing to have #ifdef or switch logic scattered around. The Factory Method thus promotes the Open/Closed Principle – allowing new types to be added with minimal changes to existing code.
Modern C++ considerations: By using smart pointers (std::unique_ptr here), we ensure that dynamically allocated objects are owned and freed automatically, preventing resource leaks. In older C++ code (pre-C++11), one often saw factories returning raw pointers that the caller had to delete, which was error-prone. C++17 doesn’t fundamentally change the factory pattern itself, but it provides convenience (like make_unique) and encourages safer memory management. Another modern twist is using std::function or templates to implement callable factories, but those are beyond the scope of this discussion.
Pros: The Factory pattern provides flexibility in choosing product implementations at runtime or configuration time. It decouples object creation from object usage; the client code only deals with interfaces or base classes. This results in code that is easier to extend (new products can be added without modifying the creator logic heavily). In my own projects, I often employ factories when I anticipate the need for swapping components (for instance, different algorithms or drivers) – it has saved me from refactoring when requirements change.
Cons: A potential downside is added complexity – the indirection of a factory can make code slightly harder to trace. If overused for simple cases, it may be over-engineering. Also, if many product types need to be supported, the factory logic (like the switch in createShape) might need updates for each new type, which is a maintenance consideration. Nonetheless, this pattern is generally considered beneficial rather than harmful; it’s commonly used in C++ frameworks and libraries. The key is to use it in scenarios where creation logic truly needs abstraction.
(Personal perspective: I find the Factory Method pattern elegant for library and framework design. It lets me provide extension points – users can subclass and override a creation method to plug in custom objects. However, if all I need is a simple helper function to create an object, I might not introduce an entire class hierarchy for the factory. In C++, sometimes a free function as shown above is enough to reap the benefits of the pattern without ceremony.)
Observer Pattern
The Observer is a behavioral design pattern that allows an object (called the subject or publisher) to maintain a list of dependents (observers or subscribers) and notify them automatically of any state changes, usually by calling one of their methods. It’s essentially a publish-subscribe mechanism: observers subscribe to a subject to receive updates, and can unsubscribe when they no longer want those updates. This pattern promotes loose coupling, as the subject doesn’t need to know the concrete classes of observers – only that they adhere to some observer interface. Observers can react to events in the subject without the subject depending on their implementation.
In C++17, implementing the Observer pattern involves creating an interface (abstract class) for observers, which declares an update method (often called update() or Notify() with some relevant parameters). The subject (which can be any class we want to observe) maintains a collection (e.g., std::vector or std::list) of pointers or references to observers. When the subject’s state changes or some interesting event occurs, it iterates through the collection and calls the update method on each observer, typically passing along any necessary data. Observers can attach (subscribe) or detach (unsubscribe) themselves from the subject.
Let’s illustrate this with a simple example. Imagine we have a Subject that holds an integer state, and we have observers that print the state whenever it changes. We’ll implement a minimal Observer pattern in C++17:
#include <vector>
#include <algorithm>
#include <iostream> // Observer interface declaring the update callback
struct IObserver {
virtual ~IObserver() = default;
virtual void update(int newState) = 0;
};
// Subject
class which observers can subscribe to
class Subject {
public: void attach(IObserver* obs) {
observers_.push_back(obs);
} void detach(IObserver* obs) {
observers_.erase(std::remove(observers_.begin(), observers_.end(), obs), observers_.end());
} void setState(int value) {
state_ = value;
notify();
// notify observers whenever state changes
}
private: void notify() {
for (IObserver* obs : observers_) {
obs->update(state_);
}
} int state_{0};
std::vector<IObserver*> observers_;
};
// A concrete observer that prints the state
class PrintingObserver : public IObserver {
public: explicit PrintingObserver(Subject& subj) : subject_(subj) {
subject_.attach(this);
} void update(int newState) override {
std::cout << "PrintingObserver: Subject state changed to " << newState << "\n";
} void unsubscribe() {
subject_.detach(this);
}
private: Subject& subject_;
};
int main() {
Subject subject;
PrintingObserver obs1(subject);
PrintingObserver obs2(subject);
subject.setState(42);
// Output (order may vary): // PrintingObserver: Subject state changed to 42 // PrintingObserver: Subject state changed to 42 obs2.unsubscribe();
// Observer 2 unsubscribes subject.setState(7);
// Output: // PrintingObserver: Subject state changed to 7 return 0;
}
In this example, Subject manages a list of IObserver*. The attach method adds an observer, and detach removes an observer (using the erase-remove idiom on the vector). The setState method (representing a change in the subject’s state) assigns the new value and then calls notify(), which in turn calls each observer’s update function with the new state. The PrintingObserver is a simple concrete observer that subscribes to a Subject upon construction (storing a reference to the subject and attaching itself), and prints the new state in its update implementation. It also provides an unsubscribe method, which detaches itself from the subject.
A few points to note in modern C++ context:
-
We used raw pointers to observers for simplicity. In real-world usage, one must ensure that observers detach themselves (or the subject removes them) before they are destroyed, to avoid dangling pointers. We demonstrated this with
obs2.unsubscribe()beforeobs2goes out of scope. Smart pointers (likestd::weak_ptrused in combination withstd::shared_ptrsubjects) can help manage lifetimes in a more robust way, but that introduces more complexity. The manual approach shown is clear for didactic purposes. -
The observer list is a
std::vector. We could have usedstd::listas well (which might simplify removal if we had iterators to elements), but using a vector andstd::removeis fine here. Performance-wise, notifying observers is O(n) in the number of observers – which is usually acceptable since the whole purpose is to update all observers. -
This pattern is very common in GUI frameworks and event systems. For example, a button (Subject) might allow observers to register for a “onClick” event, and when the button is clicked, it notifies all registered listeners. In modern C++ applications, the Observer pattern can be implemented using signals and slots (as in Qt framework) or with std::function callbacks. One modern approach is to have the subject keep a list of
std::functioncallbacks instead of class observer objects – effectively a light-weight way to achieve the same result. That said, the classic implementation using an interface is still perfectly valid and clear in intent.
Pros: The Observer pattern decouples the subject from the observers, allowing a flexible one-to-many notification system. Observers can be added or removed at runtime without modifying the subject’s code. In my experience, this leads to very extensible designs – you can add new reaction behaviors to events by simply writing new observer classes, without touching the subject. This pattern is essential in event-driven programming; as the refactoring.guru site notes, it’s common in C++ especially in the context of GUI components where you want to react to user actions or other events. I often utilise observers for logging, monitoring, or UI updates in response to state changes in the core logic.
Cons: The primary drawback is potential complexity in managing observer lifetimes and ensuring they are properly synchronized with the subject. Memory leaks or crashes can occur if an observer is not unsubscribed and gets destroyed (dangling pointer in subject), or if the subject outlives observers in some unexpected way. It can also be harder to trace the flow of a program because observers react implicitly to state changes – which can sometimes lead to spaghetti flow if overused. Another consideration is performance: if a subject has many observers or notifications are very frequent, the updates could become a bottleneck, though for most cases this is not an issue. Overall, I find the benefits outweigh the downsides, especially with careful engineering. The pattern embodies the Open/Closed Principle by allowing new observers to be added without changing the subject, and improves modularity by removing direct dependencies between the subject and specific reaction code.
(Personal perspective: I frequently apply the Observer pattern and its variants. For instance, I might use it to decouple a core simulation from a GUI display – the simulation (subject) notifies listeners when data changes, and the GUI (observer) updates the view. This keeps the simulation unaware of any UI, which is a clean separation of concerns. One piece of advice: document the observer relationships in your codebase; it helps others (and your future self) understand the event flow.)
Visitor Pattern
The Visitor is a behavioral design pattern that allows you to define new operations on a set of objects without changing the classes of those objects. In other words, Visitor lets you separate an algorithm from the object structure it operates on, by encapsulating the algorithm in a separate object (the visitor). This is useful when you have a stable set of classes (elements) and you need to perform various unrelated operations on them, which would clutter the classes themselves or violate single-responsibility if added directly. The classic example is an object structure like an Abstract Syntax Tree or a composite graphics scene: you might want to perform operations like pretty-printing, type-checking, evaluating, or exporting – tasks that are conceptually separate from the data structure itself. Using the Visitor pattern, you can add these operations without modifying the nodes of the tree; instead, you create new Visitor classes.
The Visitor pattern involves multiple key components:
-
Element (Visitable): an interface or abstract class that declares an
accept(Visitor&)method. Concrete elements (the classes on which we want to perform operations) implementacceptby calling the appropriate method on the Visitor (visitor.visit(*this)). -
Visitor: an interface that declares a visit method for each type of concrete element. For example, if we have
CircleandSquareelement classes, the Visitor interface will havevisitCircle(const Circle&)andvisitSquare(const Square&)(names can vary, but the idea is each overload handles a different type). -
Concrete Visitors: classes that implement the Visitor interface, providing the specific operation logic for each element type.
This setup achieves a form of double dispatch: the call element.accept(visitor) results in visitor.visitConcreteElement(element), thus the operation executed depends on both the visitor type and the element type. It’s more verbose than a simple virtual function call, but it allows adding new visitors (operations) without touching the element classes. The cost is that adding a new element class does require modifying all existing visitors (to add a new visit method), so the pattern is best when the set of element classes is fixed or changes infrequently, while new operations are expected to be added often.
Let’s illustrate the Visitor pattern with a simple example using shapes, where we want to perform multiple operations on those shapes. We will use two shape classes and two visitor implementations for demonstration:
#include <iostream>
#include <cmath> // Forward declarations of element classes for Visitor interface
class Circle;
class Square;
// Visitor interface with overloads for each concrete element type
class ShapeVisitor {
public: virtual ~ShapeVisitor() = default;
virtual void visit(const Circle& c) = 0;
virtual void visit(const Square& s) = 0;
};
// Element interface with accept method
class Shape {
public: virtual ~Shape() = default;
virtual void accept(ShapeVisitor& visitor) const = 0;
};
// Concrete element: Circle
class Circle : public Shape {
public: Circle(double radius) : radius_(radius) {} double getRadius() const {
return radius_;
} void accept(ShapeVisitor& visitor) const override {
visitor.visit(*this);
}
private: double radius_;
};
// Concrete element: Square
class Square : public Shape {
public: Square(double side) : side_(side) {} double getSide() const {
return side_;
} void accept(ShapeVisitor& visitor) const override {
visitor.visit(*this);
}
private: double side_;
};
// Concrete Visitor 1: compute area of shapes
class AreaVisitor : public ShapeVisitor {
public: void visit(const Circle& c) override {
double area = M_PI * std::pow(c.getRadius(), 2);
std::cout << "AreaVisitor: Area of circle = " << area << "\n";
} void visit(const Square& s) override {
double area = std::pow(s.getSide(), 2);
std::cout << "AreaVisitor: Area of square = " << area << "\n";
}
};
// Concrete Visitor 2: print shape type
class TypeVisitor : public ShapeVisitor {
public: void visit(const Circle& c) override {
std::cout << "TypeVisitor: This is a circle with radius " << c.getRadius() << "\n";
} void visit(const Square& s) override {
std::cout << "TypeVisitor: This is a square with side " << s.getSide() << "\n";
}
};
int main() {
Circle circle(5.0);
Square square(3.0);
AreaVisitor areaCalc;
TypeVisitor typePrinter;
// Use AreaVisitor circle.accept(areaCalc);
square.accept(areaCalc);
// Use TypeVisitor circle.accept(typePrinter);
square.accept(typePrinter);
return 0;
}
Output:
AreaVisitor: Area of circle = 78.5398 AreaVisitor: Area of square = 9 TypeVisitor: This is a circle with radius 5 TypeVisitor: This is a square with side 3
In this code, Shape is the element base class with a pure virtual accept method. Circle and Square implement accept by calling the appropriate visit method on the visitor. The ShapeVisitor interface has two overloads visit(const Circle&) and visit(const Square&). AreaVisitor and TypeVisitor are two different operations packaged as visitor objects – one computes and prints area, the other prints the type. Notice how circle.accept(areaCalc) ends up calling AreaVisitor::visit(const Circle&) because the Circle class directs it to that overload, whereas square.accept(areaCalc) calls AreaVisitor::visit(const Square&). We have effectively separated the operations from the shape classes. Adding a new operation is as simple as writing a new visitor class (for instance, a PerimeterVisitor could be added without changing Circle or Square classes at all).
A key aspect of Visitor is that it relies on all possible element types being known and handled in the Visitor interface. If we introduced a new class, say Triangle, we would need to add virtual void visit(const Triangle&) to ShapeVisitor and implement it in all Concrete Visitors. This is why the pattern is ideal when your class hierarchy is stable. It trades off flexibility in extending classes for flexibility in adding operations.
Use cases: As mentioned, Visitors are handy for operations on complex object structures like abstract syntax trees (compilers often use visitors for traversing AST nodes), object serialization, UI component trees (applying an operation to all UI elements), etc. The pattern isn’t very common in everyday application code because it is somewhat complex and overkill unless you truly need that level of flexibility. Personally, I’ve used Visitor in scenarios like writing a compiler where multiple analyses and transformations had to be performed on a tree of nodes – implementing each analysis as a Visitor kept concerns separate.
Modern C++ considerations: C++17 did not change the Visitor pattern directly, but it introduced std::variant and std::visit, which provide an alternative for some use cases. A std::variant is a type-safe union that can hold one of several types, and std::visit can apply a callable to the currently held value. This can be seen as a strategy for structuring code similar to a visitor: instead of a class hierarchy with virtual dispatch, you use a variant and visitors in the form of function objects or lambdas. For example, rather than having Shape as a base class, one could have using ShapeVar = std::variant<Circle, Square>; and use std::visit with a lambda that handles each type. This approach leverages the compiler to generate the dispatch logic. In C++17, std::visit with variants allows implementing the visitor pattern in a type-safe and succinct way. It’s not a drop-in replacement for all scenarios (especially if the object set is open-ended or requires inheritance), but for closed sets of types it works brilliantly. The Modernes C++ blog points out that std::visit effectively acts as a visitor on a variant, with the visitor being any callable that has an overload for each variant alternative.
Pros: The Visitor pattern excels at adding new operations to existing object structures. It keeps operations outside of the objects, adhering to single responsibility (the objects manage their data, the visitors implement the algorithms). It also groups related operations into one visitor class, which can make the code more organized than scattering the logic across many classes. In scenarios where you frequently need to extend functionality, visitors provide a clear protocol for doing so. The double-dispatch mechanism is powerful – something not directly available in C++ without this pattern.
Cons: The pattern is considered one of the more complex GOF patterns. It introduces multiple classes and tight coupling between the Visitor and Element hierarchies (each knows about the other’s interface). Adding a new element type requires updating all existing visitors, so it’s not flexible in that dimension. If your set of types changes often, Visitor can become burdensome. Additionally, the boilerplate (like writing accept methods and visitor interfaces) can be tedious – though templates or code generators can help. Some in the C++ community avoid Visitor when possible, noting that modern C++ alternatives (like variants or even just virtual functions in the base class for each operation) may be simpler for a given problem. I share the view that Visitor should be used when it fits really well – otherwise simpler polymorphic or functional approaches might suffice. That said, when I have a scenario that matches the Visitor’s strengths (lots of operations, stable structure), I find it to be an elegant solution.
(Personal perspective: I regard the Visitor pattern as a “power tool” – not needed often, but very useful in the right situation. My advice is to evaluate if the complexity is justified by your use case. If you foresee adding many operations over time and you cannot or prefer not to bake them into the classes (perhaps to keep those classes lightweight), Visitor is worth it. If not, you might keep things simple. C++17’s std::variant provides a nice middle ground for some problems; I’ve enjoyed using it with std::visit and lambda overloads to handle multiple types without an explicit visitor class, which feels lighter-weight while achieving a similar outcome.)
Conclusion
In this article, we examined four common design patterns and how to implement them in modern C++17: Singleton, Factory Method, Observer, and Visitor. Each pattern addresses a particular kind of design challenge – controlling object creation, abstracting instantiation, managing publish/subscribe relationships, and adding operations to class hierarchies without modifying them. The C++17 standard, while not altering these patterns’ core principles, provides language features (smart pointers, inline variables, std::variant, etc.) that make implementations safer, cleaner, or more expressive. For instance, C++’s guaranteed thread-safe static initialization makes Singleton easier to implement safely, and smart pointers like unique_ptr encourage better memory management in factories.
It’s important to recognise that design patterns are abstractions that can sometimes be superseded by language features. There’s a well-known criticism that many GoF patterns are “workarounds for missing features” in the language. Indeed, modern C++ techniques (template metaprogramming, lambdas, std::function, variants, ranges, etc.) can simplify or eliminate some patterns. For example, some uses of Factory can be replaced with templates or object factories embedded in template parameters; simple Observers can be done with signals or event delegates; and as discussed, a std::variant with std::visit can handle some scenarios that might otherwise call for a Visitor. C++ is multi-paradigm, so sometimes a generic or functional approach obviates an OOP pattern solution.
That said, I believe knowing these classic patterns is still extremely valuable. They teach fundamental design principles like encapsulation, separation of concerns, and polymorphism. In large codebases or team projects, communicating using pattern names (“Let’s use an Observer here” or “This class is a Singleton responsible for…”) provides a shared vocabulary that makes architectural discussions more efficient. The key is to apply patterns judiciously – understand the problem at hand and choose a pattern or modern C++ feature that best solves it with clarity and minimal complexity. As we’ve seen, each pattern comes with trade-offs. By critically analyzing those pros and cons (as we did above), one can make informed decisions.
In conclusion, design patterns remain highly relevant in C++17 and beyond, even if the implementation details evolve. Modern C++ has empowered us to implement these patterns in cleaner and safer ways, and sometimes to even avoid a pattern with a direct language feature. My own approach is to use patterns as guidelines, not straightjackets: I adapt them to modern C++ idioms and only when they genuinely make the design better. By doing so, we can harness the wisdom of classic design patterns while writing code that is efficient, safe, and robust in C++17.
References
-
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. (GoF book introducing Singleton, Factory Method, Observer, Visitor, etc.)
-
Refactoring.Guru – Singleton Design Pattern (Intent and overview)refactoring.gururefactoring.guru.
-
Refactoring.Guru – Factory Method in C++ (Intent and usage in C++)refactoring.gururefactoring.guru.
-
Refactoring.Guru – Observer Design Pattern in C++ (Concept and usage)refactoring.gururefactoring.guru.
-
Refactoring.Guru – Visitor Design Pattern in C++ (Intent and usage)refactoring.gururefactoring.guru.
-
Krzaq (Stack Overflow answer) – C++11 Thread-safe initialization of function-local static objects (explains magic statics in C++11)stackoverflow.com.
-
cppreference.com – std::unique_ptr (smart pointer managing object lifetime)en.cppreference.com.
-
Richard Haar – Using the Visitor pattern in C++17 (discussion of
std::variantandstd::visitas an implementation of Visitor)richhaar.com. -
Modernes C++ (R. Grimm) – C++17 Inline static in Singleton (inlining static members in-class)modernescpp.com.
-
Wikipedia (cited in Stack Overflow) – Critique of GoF Patterns as workarounds in less powerful languagesstackoverflow.com.