Introduction: SOLID in the Context of C++
SOLID is an acronym for five fundamental principles of object-oriented design formulated by Robert Martin. For C++, as a language with powerful static typing, multiple inheritance, and RAII, these principles take on special significance. At an advanced level, understanding SOLID allows you to create flexible, testable, and extensible systems, avoiding antipatterns such as the "god class" or tight coupling.
In this article, we will break down each principle with practical C++ (17/20) code examples, focusing on implementation nuances: the use of std::unique_ptr, std::function, CRTP, virtual destructors, and modern idioms.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change.
In practice, this means a class is responsible for one business entity or one operation. Violating SRP leads to "coupling" of responsibilities, complicating testing and modification.
Example of Violation
class Report {public: void generate() { /* data generation */ } void saveToFile(const std::string& path) { /* file writing */ } void sendEmail(const std::string& to) { /* sending */ }};Here, Report is responsible for generation, saving, and sending — three reasons to change.
Refactoring with SRP
class Report {public: void generate() { /* data generation */ }};
class ReportSaver {public: void save(const Report& report, const std::string& path);};
class EmailSender {public: void send(const Report& report, const std::string& to);};Now each class has a single responsibility. This simplifies unit testing and reuse.
2. Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
In C++, this is often implemented through polymorphism (virtual functions) or templates (CRTP). For an advanced level, we will consider both techniques.
Polymorphic Approach (Virtual Functions)
class Shape {public: virtual double area() const = 0; virtual ~Shape() = default;};
class Circle : public Shape { double r;public: Circle(double r) : r(r) {} double area() const override { return 3.14 * r * r; }};
class Rectangle : public Shape { double w, h;public: Rectangle(double w, double h) : w(w), h(h) {} double area() const override { return w * h; }};Adding a new shape (e.g., Triangle) does not require modifying existing classes — only creating a new derived class.
Template Approach (CRTP — Curiously Recurring Template Pattern)
template<typename Derived>class ShapeBase {public: double area() const { return static_cast<const Derived*>(this)->area_impl(); }};
class Circle : public ShapeBase<Circle> { double r;public: Circle(double r) : r(r) {} double area_impl() const { return 3.14 * r * r; }};CRTP provides static polymorphism without the overhead of virtual calls, which is critical for high-performance C++.
3. Liskov Substitution Principle (LSP)
Definition: Objects of a base class should be replaceable with objects of derived classes without breaking the correctness of the program.
A classic violation of LSP in C++ is overriding a virtual method by weakening preconditions or strengthening postconditions. Let's consider an example with the "Rectangle — Square" hierarchy.
Antipattern
class Rectangle {protected:\