Introduction to Design Patterns in C++
Design patterns are time-tested architectural solutions for typical development tasks. In the world of C++, they are especially important as they allow creating flexible, extensible, and maintainable code. In this article, we will cover four fundamental patterns: Singleton, Factory Method, Observer, and Strategy — with complete examples in modern C++ (C++17/20).
Each pattern will be accompanied by a practical example, a class diagram, and explanations of its application. The material is intended for developers already familiar with the basics of C++ and OOP.
1. Creational Pattern Singleton
Singleton guarantees that a class has only one instance and provides a global point of access to it. In C++, the classic implementation uses a static method and a private constructor.
When to apply
- Managing a database connection
- Logging system
- Cache manager
- Application configuration
Implementation example (thread-safe Singleton)
#include <iostream>#include <mutex>#include <memory>
class Logger {private: static std::unique_ptr<Logger> instance; static std::mutex mtx; Logger() { std::cout << "Logger created" << std::endl; } public: Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete; static Logger* getInstance() { std::lock_guard<std::mutex> lock(mtx); if (!instance) { instance.reset(new Logger()); } return instance.get(); } void log(const std::string& msg) { std::cout << "[LOG] " << msg << std::endl; }};
std::unique_ptr<Logger> Logger::instance = nullptr;std::mutex Logger::mtx;
int main() { Logger* log1 = Logger::getInstance(); Logger* log2 = Logger::getInstance(); log1->log("Test message"); // log1 and log2 point to the same object std::cout << "log1 == log2: " << (log1 == log2) << std::endl; return 0;}Modern C++ approach (Meyers' Singleton)
class ConfigManager {public: static ConfigManager& getInstance() { static ConfigManager instance; // thread-safe in C++11+ return instance; } void setParam(const std::string& key, const std::string& value) { params[key] = value; } std::string getParam(const std::string& key) { return params[key]; } private: ConfigManager() = default; std::map<std::string, std::string> params;};2. Creational Pattern Factory Method
Factory Method defines an interface for creating an object but lets subclasses decide which concrete class to instantiate. This pattern is especially useful when the type of object is not known in advance.
Pattern structure
- Product — abstract product class
- ConcreteProduct — concrete product implementation
- Creator — abstract creator class with a factory method
- ConcreteCreator — overrides the factory method
Example: notification system
#include <iostream>#include <memory>#include <string>
// Abstract productclass Notification {public: virtual void send(const std::string& message) = 0; virtual ~Notification() = default;};
// Concrete productsclass EmailNotification : public Notification {public: void send(const std::str