Introduction: Why Design Patterns are Needed in C++
Design patterns are proven solutions to typical problems that arise during software development. In C++, with its powerful type system, RAII, and multiple inheritance, patterns gain special flexibility. They help make code more readable, maintainable, and scalable.
In this article, we will look at 4 key patterns from the GoF (Gang of Four) catalog: Singleton, Factory Method, Observer, and Strategy. Each pattern will be illustrated with a real code example in C++17/20.
Creational Patterns: Singleton
Singleton guarantees that a class has only one instance and provides a global access point to it. In C++, the modern approach uses a local static variable (thread-safe since C++11).
Example: Application Logger
class Logger {public: // Delete copy and assignment Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete;
static Logger& getInstance() { static Logger instance; // thread-safe since C++11 return instance; }
void log(const std::string& message) { std::lock_guard<std::mutex> lock(m_mutex); std::cout << "[LOG] " << message << std::endl; }
private: Logger() = default; // private constructor ~Logger() = default; std::mutex m_mutex;};
// Usageint main() { Logger::getInstance().log("Application started"); Logger::getInstance().log("Error: file not found"); return 0;}Pros: guaranteed uniqueness, lazy initialization, thread safety.
Cons: complicates testing (global state), violates the single responsibility principle.
Creational Patterns: Factory Method
Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate. In C++, it is often implemented through virtual functions and smart pointers.
Example: Generating Different Document Types
// Abstract productclass Document {public: virtual void open() = 0; virtual void save() = 0; virtual ~Document() = default;};
// Concrete productsclass PDFDocument : public Document {public: void open() override { std::cout << "PDF opened"; } void save() override { std::cout << "PDF saved"; }};
class WordDocument : public Document {public: void open() override { std::cout << "Word opened"; } void save() override { std::cout << "Word saved"; }};
// Creatorclass Application {public: virtual std::unique_ptr<Document> createDocument() = 0; virtual ~Application() = default;
void newDocument() { auto doc = createDocument(); doc->open(); // work with document }};
// Concrete creatorsclass PDFApplication : public Application {public: std::unique_ptr<Document> createDocument() override { return std::make_unique<PDFDocument>(); }};
class WordApplication : public Application {public: std::unique_ptr<Document> createDocument() override { return std::make_unique<WordDocument>(); }};
// Usageint main() { std::unique_ptr<Application> app = std::make_unique<PDFApplication>(); app->newDocument(); // Output: "PDF opened" return 0;}This pattern is ideal when the exact types of objects to be created are not known in advance.
Behavioral Patterns: Observer
Blogs
Book Recommendations
Read also
PHP Application Security: Top 10 Vulnerabilities for Beginners
Best IDEs and Tools for PHP Development in 2024
Integrating WebAssembly and C#: Building High-Performance Web Applications
How Long to Learn Python: Realistic Timeline to Junior
Number System Conversions and Working with Numbers in Strings