Design patterns are like architectural blueprints for software. Just as architects reuse proven building designs (like the ranch house or Victorian style), programmers use design patterns to solve common problems elegantly.
The Singleton ensures only one instance of a class exists, like having only one president of a country at a time.
// Modern C++ Singleton using static local variable
class Logger {
private:
Logger() {
// Private constructor
std::cout << "Logger created\n";
}
public:
// Delete copy/move operations
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
// Thread-safe in C++11 and later
static Logger& getInstance() {
static Logger instance; // Created once, destroyed at program end
return instance;
}
void log(const std::string& message) {
// Thread-safe logging would require additional synchronization
std::cout << "[LOG] " << message << std::endl;
}
};
// Usage
Logger::getInstance().log("Application started");
// Logger::getInstance() always returns the same instance
Factory pattern is like a restaurant menu - you order by name ("pizza") without knowing the exact preparation details.
// Abstract Product
class Vehicle {
public:
virtual ~Vehicle() = default;
virtual void drive() const = 0;
virtual std::string getType() const = 0;
};
// Concrete Products
class Car : public Vehicle {
public:
void drive() const override {
std::cout << "Driving a car on the road\n";
}
std::string getType() const override { return "Car"; }
};
class Motorcycle : public Vehicle {
public:
void drive() const override {
std::cout << "Riding a motorcycle\n";
}
std::string getType() const override { return "Motorcycle"; }
};
// Modern Factory using unique_ptr and variadic templates
class VehicleFactory {
public:
// Template factory method
template<typename T, typename... Args>
static std::unique_ptr<Vehicle> create(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
// String-based factory
static std::unique_ptr<Vehicle> create(const std::string& type) {
if (type == "car") {
return std::make_unique<Car>();
} else if (type == "motorcycle") {
return std::make_unique<Motorcycle>();
}
return nullptr;
}
};
// Usage
auto car = VehicleFactory::create<Car>();
auto moto = VehicleFactory::create("motorcycle");
car->drive();
Observer pattern is like a newsletter subscription - subscribers get notified when new content is published.
// Modern Observer using function objects and weak_ptr
template<typename EventType>
class EventEmitter {
private:
using Handler = std::function<void(const EventType&)>;
using HandlerPtr = std::shared_ptr<Handler>;
mutable std::mutex mutex_;
std::vector<std::weak_ptr<Handler>> handlers_;
public:
class Subscription {
friend class EventEmitter;
std::weak_ptr<Handler> handler_;
EventEmitter* emitter_;
Subscription(std::weak_ptr<Handler> h, EventEmitter* e)
: handler_(h), emitter_(e) {}
public:
void unsubscribe() {
if (auto h = handler_.lock()) {
emitter_->remove(h);
}
}
};
Subscription subscribe(Handler handler) {
std::lock_guard<std::mutex> lock(mutex_);
auto ptr = std::make_shared<Handler>(std::move(handler));
handlers_.push_back(ptr);
return Subscription(ptr, this);
}
void emit(const EventType& event) const {
std::lock_guard<std::mutex> lock(mutex_);
// Clean up expired handlers
auto it = std::remove_if(handlers_.begin(), handlers_.end(),
[](const std::weak_ptr<Handler>& h) { return h.expired(); });
handlers_.erase(it, handlers_.end());
// Notify active handlers
for (const auto& weakHandler : handlers_) {
if (auto handler = weakHandler.lock()) {
(*handler)(event);
}
}
}
private:
void remove(const HandlerPtr& target) {
std::lock_guard<std::mutex> lock(mutex_);
handlers_.erase(
std::remove_if(handlers_.begin(), handlers_.end(),
[&](const std::weak_ptr<Handler>& h) {
auto ptr = h.lock();
return !ptr || ptr == target;
}),
handlers_.end()
);
}
};
// Usage Example
struct PriceChangeEvent {
std::string symbol;
double oldPrice;
double newPrice;
};
class StockMarket {
EventEmitter<PriceChangeEvent> priceChanged;
public:
auto onPriceChange(std::function<void(const PriceChangeEvent&)> handler) {
return priceChanged.subscribe(std::move(handler));
}
void updatePrice(const std::string& symbol, double newPrice) {
double oldPrice = getPrice(symbol);
setPrice(symbol, newPrice);
priceChanged.emit({symbol, oldPrice, newPrice});
}
private:
std::map<std::string, double> prices_;
double getPrice(const std::string& symbol) { /* ... */ }
void setPrice(const std::string& symbol, double price) { /* ... */ }
};
Strategy pattern is like choosing a route to work - you can walk, drive, or take the bus, each with different characteristics.
// Modern Strategy using std::function and lambdas
class DataProcessor {
private:
using ProcessingStrategy = std::function<std::vector<int>(const std::vector<int>&)>;
ProcessingStrategy strategy_;
public:
void setStrategy(ProcessingStrategy strategy) {
strategy_ = std::move(strategy);
}
std::vector<int> process(const std::vector<int>& data) {
if (!strategy_) {
throw std::logic_error("No strategy set");
}
return strategy_(data);
}
};
// Predefined strategies
namespace Strategies {
auto sortAscending = [](const std::vector<int>& data) {
auto result = data;
std::sort(result.begin(), result.end());
return result;
};
auto sortDescending = [](const std::vector<int>& data) {
auto result = data;
std::sort(result.rbegin(), result.rend());
return result;
};
auto removeDuplicates = [](const std::vector<int>& data) {
auto result = data;
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
};
auto filterEven = [](const std::vector<int>& data) {
std::vector<int> result;
std::copy_if(data.begin(), data.end(), std::back_inserter(result),
[](int n) { return n % 2 == 0; });
return result;
};
}
// Usage
DataProcessor processor;
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};
processor.setStrategy(Strategies::sortAscending);
auto sorted = processor.process(data); // [1, 1, 2, 3, 4, 5, 6, 9]
processor.setStrategy(Strategies::removeDuplicates);
auto unique = processor.process(data); // [1, 2, 3, 4, 5, 6, 9]
// Custom strategy on the fly
processor.setStrategy([](const auto& data) {
std::vector<int> result;
for (int n : data) {
result.push_back(n * n); // Square each element
}
return result;
});
auto squared = processor.process(data);
Decorator pattern is like adding toppings to a pizza - each topping adds to the base without changing its fundamental nature.
// Modern Decorator using templates and perfect forwarding
template<typename Component>
class TextProcessor {
public:
virtual ~TextProcessor() = default;
virtual std::string process(const std::string& text) const = 0;
};
// Base component
class PlainText : public TextProcessor<PlainText> {
public:
std::string process(const std::string& text) const override {
return text;
}
};
// Decorator base class
template<typename Component>
class TextDecorator : public TextProcessor<Component> {
protected:
std::unique_ptr<TextProcessor<Component>> component_;
public:
explicit TextDecorator(std::unique_ptr<TextProcessor<Component>> component)
: component_(std::move(component)) {}
};
// Concrete decorators
template<typename Component>
class UpperCaseDecorator : public TextDecorator<Component> {
public:
using TextDecorator<Component>::TextDecorator;
std::string process(const std::string& text) const override {
auto result = this->component_->process(text);
std::transform(result.begin(), result.end(), result.begin(), ::toupper);
return result;
}
};
template<typename Component>
class TrimDecorator : public TextDecorator<Component> {
public:
using TextDecorator<Component>::TextDecorator;
std::string process(const std::string& text) const override {
auto result = this->component_->process(text);
// Trim leading/trailing whitespace
result.erase(0, result.find_first_not_of(" \t\n\r"));
result.erase(result.find_last_not_of(" \t\n\r") + 1);
return result;
}
};
// Fluent builder for easy decoration
template<typename T>
class TextProcessorBuilder {
std::unique_ptr<TextProcessor<T>> processor_;
public:
TextProcessorBuilder() : processor_(std::make_unique<PlainText>()) {}
TextProcessorBuilder& upperCase() {
processor_ = std::make_unique<UpperCaseDecorator<T>>(std::move(processor_));
return *this;
}
TextProcessorBuilder& trim() {
processor_ = std::make_unique<TrimDecorator<T>>(std::move(processor_));
return *this;
}
std::unique_ptr<TextProcessor<T>> build() {
return std::move(processor_);
}
};
// Usage
auto processor = TextProcessorBuilder<PlainText>()
.trim()
.upperCase()
.build();
std::string result = processor->process(" hello world ");
// Result: "HELLO WORLD"
RAII (Resource Acquisition Is Initialization) is like hiring a cleaning service that automatically cleans when they leave - resources are automatically managed.
// Custom RAII wrapper for file handles
class FileHandle {
private:
FILE* file_;
public:
explicit FileHandle(const std::string& filename, const char* mode)
: file_(std::fopen(filename.c_str(), mode)) {
if (!file_) {
throw std::runtime_error("Failed to open file: " + filename);
}
}
~FileHandle() {
if (file_) {
std::fclose(file_);
}
}
// Delete copy operations
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Move operations
FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
other.file_ = nullptr;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (file_) std::fclose(file_);
file_ = other.file_;
other.file_ = nullptr;
}
return *this;
}
FILE* get() const { return file_; }
operator FILE*() const { return file_; }
};
// Custom deleter for unique_ptr
struct SocketDeleter {
void operator()(int* socket) const {
if (socket && *socket >= 0) {
close(*socket);
delete socket;
}
}
};
using SocketPtr = std::unique_ptr<int, SocketDeleter>;
// Shared resource with custom deleter
auto createSharedBuffer(size_t size) {
return std::shared_ptr<uint8_t[]>(
new uint8_t[size],
[](uint8_t* p) {
std::cout << "Deallocating buffer\n";
delete[] p;
}
);
}
The Curiously Recurring Template Pattern (CRTP) provides compile-time polymorphism, like a recipe template where specific steps are filled in by the chef.
// CRTP base class for game entities
template<typename Derived>
class GameObject {
public:
void update(float deltaTime) {
// Common pre-update logic
if (!isActive()) return;
// Derived-specific update
static_cast<Derived*>(this)->onUpdate(deltaTime);
// Common post-update logic
updateAnimation(deltaTime);
}
void render() {
if (!isVisible()) return;
beginRender();
static_cast<Derived*>(this)->onRender();
endRender();
}
protected:
bool isActive() const { return active_; }
bool isVisible() const { return visible_; }
private:
bool active_ = true;
bool visible_ = true;
void updateAnimation(float deltaTime) { /* ... */ }
void beginRender() { /* ... */ }
void endRender() { /* ... */ }
};
// Concrete implementations
class Player : public GameObject<Player> {
friend class GameObject<Player>;
void onUpdate(float deltaTime) {
// Player-specific update logic
handleInput();
movePlayer(deltaTime);
}
void onRender() {
// Player-specific rendering
drawSprite(sprite_, position_);
}
void handleInput() { /* ... */ }
void movePlayer(float deltaTime) { /* ... */ }
};
class Enemy : public GameObject<Enemy> {
friend class GameObject<Enemy>;
void onUpdate(float deltaTime) {
// Enemy-specific update logic
updateAI(deltaTime);
moveTowardsPlayer(deltaTime);
}
void onRender() {
// Enemy-specific rendering
drawAnimatedSprite(animation_, position_);
}
void updateAI(float deltaTime) { /* ... */ }
void moveTowardsPlayer(float deltaTime) { /* ... */ }
};
The visitor pattern allows operations on objects without modifying their classes. With std::variant, it's like having a universal remote that works differently for each device.
// Define shape types
struct Circle {
double radius;
};
struct Rectangle {
double width, height;
};
struct Triangle {
double base, height;
};
// Shape variant
using Shape = std::variant<Circle, Rectangle, Triangle>;
// Visitor for calculating area
struct AreaCalculator {
double operator()(const Circle& c) const {
return M_PI * c.radius * c.radius;
}
double operator()(const Rectangle& r) const {
return r.width * r.height;
}
double operator()(const Triangle& t) const {
return 0.5 * t.base * t.height;
}
};
// Visitor for drawing
struct ShapeDrawer {
void operator()(const Circle& c) const {
std::cout << "Drawing circle with radius " << c.radius << "\n";
}
void operator()(const Rectangle& r) const {
std::cout << "Drawing rectangle " << r.width << "x" << r.height << "\n";
}
void operator()(const Triangle& t) const {
std::cout << "Drawing triangle with base " << t.base << "\n";
}
};
// Usage
std::vector<Shape> shapes = {
Circle{5.0},
Rectangle{10.0, 20.0},
Triangle{8.0, 6.0}
};
// Calculate total area
double totalArea = 0;
for (const auto& shape : shapes) {
totalArea += std::visit(AreaCalculator{}, shape);
}
// Draw all shapes
for (const auto& shape : shapes) {
std::visit(ShapeDrawer{}, shape);
}
// Implement a command pattern for a text editor
class TextEditor {
std::string text_;
public:
void insert(size_t pos, const std::string& str) { /* ... */ }
void erase(size_t pos, size_t len) { /* ... */ }
const std::string& getText() const { return text_; }
};
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
// TODO: Implement InsertCommand and DeleteCommand
// TODO: Implement CommandHistory with undo/redo stacks
// Create a fluent builder for HTTP requests
class HttpRequest {
// Private members: method, url, headers, body, etc.
public:
class Builder {
// TODO: Implement builder methods
// Builder& method(const std::string& m);
// Builder& url(const std::string& u);
// Builder& header(const std::string& key, const std::string& value);
// Builder& body(const std::string& b);
// HttpRequest build();
};
static Builder create() { return Builder{}; }
};
// Usage should look like:
// auto request = HttpRequest::create()
// .method("POST")
// .url("https://api.example.com/data")
// .header("Content-Type", "application/json")
// .body("{\"key\": \"value\"}")
// .build();
You've learned how modern C++ enhances classic design patterns:
Design patterns in modern C++ are more flexible and efficient than ever. Smart pointers eliminate memory leaks, templates provide compile-time safety, and features like std::variant offer new ways to implement classic patterns. In our final lesson, we'll explore cutting-edge C++20 features including concepts, ranges, and coroutines.