Chapter 23: Advanced Topics
23. Advanced Topics in C++: Taking Your Skills to the Next Level
Hello again, my eager student! π Welcome to Lesson 23 β Advanced Topics β the chapter where we dive deep into the sophisticated side of C++ that powers large-scale software like game engines, financial systems, and high-performance libraries. These concepts aren’t just “nice to know”βthey’re essential for writing efficient, maintainable, and extensible code in professional environments.
Since we’ve built up from basics, I’ll explain each topic like we’re in an advanced seminar: starting with why it matters, then the core ideas, followed by detailed examples, common pitfalls, and modern best practices (C++11+ focus). We’ll use real-world analogies to make abstract ideas concrete. By the end, you’ll see how these fit together.
Let’s start!
1. Design Patterns: Reusable Solutions to Common Problems
Design patterns are proven blueprints for solving recurring problems in OOP β like recipes for software architecture. They were popularized by the “Gang of Four” book (GoF). C++ excels at patterns due to its flexibility with templates, polymorphism, and RAII.
We’ll cover four classics: Singleton, Factory, Observer, and Strategy. (There are 23 GoF patterns, but these are foundational.)
A. Singleton Pattern β Ensure Only One Instance Exists
Why it matters: For global resources like loggers, databases, or config managers β you want exactly one instance, accessible everywhere, without global variables (which are error-prone).
Core idea: Private constructor + static method to get the instance. Thread-safe in modern C++.
Detailed example β Thread-safe Singleton Logger
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <iostream> #include <mutex> #include <string> class Logger { private: static Logger* instance_; // static pointer to single instance static std::mutex mtx_; // for thread safety Logger() { // private constructor std::cout << "Logger initialized\n"; } ~Logger() { std::cout << "Logger destroyed\n"; } // Delete copy/move to prevent duplicates Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete; Logger(Logger&&) = delete; Logger& operator=(Logger&&) = delete; public: static Logger* getInstance() { std::lock_guard<std::mutex> lock(mtx_); if (!instance_) { instance_ = new Logger(); // lazy initialization } return instance_; } void log(const std::string& message) { std::cout << "Log: " << message << "\n"; } }; // Static members definition Logger* Logger::instance_ = nullptr; std::mutex Logger::mtx_; int main() { Logger* log1 = Logger::getInstance(); Logger* log2 = Logger::getInstance(); // same instance log1->log("Error occurred"); log2->log("All good"); // No delete needed β but in real code, add shutdown return 0; } |
Output:
|
0 1 2 3 4 5 6 7 8 9 |
Logger initialized Log: Error occurred Log: All good Logger destroyed (if you add manual delete) |
Analogy: Like a single CEO in a company β everyone accesses the same person, but no one can create a duplicate.
Pitfalls & best practices:
- Thread safety: Use mutex (as above) or Meyer’s Singleton (static local variable).
- Modern alternative: Use dependency injection instead of singletons when possible (easier testing).
- Common mistake: Forgetting to delete copy constructors β multiple instances.
- C++11+ tip: Use std::call_once for even safer initialization.
B. Factory Pattern β Create Objects Without Specifying Exact Class
Why it matters: When you want to create objects based on conditions (e.g., OS-specific UI), without hardcoding types. Promotes loose coupling.
Core idea: A factory method or abstract factory class returns objects of a base type, hiding creation details.
Detailed example β Shape Factory
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <iostream> #include <memory> // for unique_ptr class Shape { public: virtual ~Shape() = default; virtual void draw() const = 0; }; class Circle : public Shape { public: void draw() const override { std::cout << "Drawing Circle\n"; } }; class Square : public Shape { public: void draw() const override { std::cout << "Drawing Square\n"; } }; class ShapeFactory { public: static std::unique_ptr<Shape> createShape(const std::string& type) { if (type == "circle") return std::make_unique<Circle>(); if (type == "square") return std::make_unique<Square>(); throw std::invalid_argument("Unknown shape type"); } }; int main() { auto shape1 = ShapeFactory::createShape("circle"); auto shape2 = ShapeFactory::createShape("square"); shape1->draw(); // Drawing Circle shape2->draw(); // Drawing Square return 0; } |
Analogy: Like a car factory β you request “sedan” or “SUV”, and it builds the right one without you knowing the assembly details.
Pitfalls & best practices:
- Use smart pointers: Return unique_ptr for ownership transfer.
- Abstract Factory variant: For families of related objects (e.g., WindowsButton + WindowsMenu).
- Common mistake: Leaking memory β use RAII/smart pointers.
- C++11+ tip: Use templates for type-safe factories.
C. Observer Pattern β Notify Multiple Objects of Changes
Why it matters: For publish-subscribe systems β like UI events (button click notifies listeners) or stock price changes notifying traders.
Core idea: Subject maintains a list of Observers and notifies them on state change.
Detailed example β Stock Price Observer
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
#include <iostream> #include <vector> #include <memory> class Observer { public: virtual ~Observer() = default; virtual void update(double price) = 0; }; class Stock { private: std::vector<std::weak_ptr<Observer>> observers_; // weak to avoid cycles double price_ = 0.0; public: void addObserver(std::shared_ptr<Observer> obs) { observers_.push_back(obs); } void setPrice(double newPrice) { price_ = newPrice; notify(); } void notify() { for (auto it = observers_.begin(); it != observers_.end(); ) { if (auto obs = it->lock()) { obs->update(price_); ++it; } else { it = observers_.erase(it); // remove expired } } } }; class Trader : public Observer { private: std::string name_; public: Trader(std::string name) : name_(std::move(name)) {} void update(double price) override { std::cout << name_ << " notified: New price = " << price << "\n"; } }; int main() { Stock appleStock; auto trader1 = std::make_shared<Trader>("Alice"); auto trader2 = std::make_shared<Trader>("Bob"); appleStock.addObserver(trader1); appleStock.addObserver(trader2); appleStock.setPrice(150.5); // notifies both trader2.reset(); // Bob leaves appleStock.setPrice(155.0); // only Alice notified return 0; } |
Output:
|
0 1 2 3 4 5 6 7 8 |
Alice notified: New price = 150.5 Bob notified: New price = 150.5 Alice notified: New price = 155 |
Analogy: Like YouTube β channel (subject) notifies subscribers (observers) of new videos.
Pitfalls & best practices:
- Memory leaks: Use weak_ptr for observers.
- Common mistake: Forgetting to remove dead observers.
- C++11+ tip: Use std::function for more flexible callbacks.
D. Strategy Pattern β Swap Algorithms at Runtime
Why it matters: For pluggable behaviors β like sorting algorithms or payment methods.
Core idea: Define a family of algorithms (strategies), encapsulate each, and make them interchangeable.
Detailed example β Payment Strategies
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <iostream> #include <memory> class PaymentStrategy { public: virtual ~PaymentStrategy() = default; virtual void pay(double amount) = 0; }; class CreditCard : public PaymentStrategy { public: void pay(double amount) override { std::cout << "Paid $" << amount << " with Credit Card\n"; } }; class PayPal : public PaymentStrategy { public: void pay(double amount) override { std::cout << "Paid $" << amount << " with PayPal\n"; } }; class ShoppingCart { private: std::unique_ptr<PaymentStrategy> strategy_; public: void setPaymentStrategy(std::unique_ptr<PaymentStrategy> strategy) { strategy_ = std::move(strategy); } void checkout(double total) { if (strategy_) { strategy_->pay(total); } else { std::cout << "No payment method set!\n"; } } }; int main() { ShoppingCart cart; cart.setPaymentStrategy(std::make_unique<CreditCard>()); cart.checkout(100.0); // Paid $100 with Credit Card cart.setPaymentStrategy(std::make_unique<PayPal>()); cart.checkout(50.0); // Paid $50 with PayPal return 0; } |
Analogy: Like switching GPS apps (Google Maps vs Waze) β same goal, different strategy.
Pitfalls & best practices:
- Use interfaces: Pure virtual base class.
- Common mistake: Tight coupling β use dependency injection.
- C++11+ tip: Use lambdas for simple strategies.
2. RAII & Smart Pointers in Depth
RAII (Resource Acquisition Is Initialization): Resources are acquired in constructors and released in destructors β automatic cleanup!
In depth:
- Applies to memory, files, locks, sockets β anything with “open/close”.
- Destructors run even on exceptions β no leaks.
- Smart pointers are RAII for heap memory.
Example β Custom RAII File Handle
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
class FileHandle { private: std::FILE* file_; public: explicit FileHandle(const char* path) { file_ = std::fopen(path, "r"); if (!file_) throw std::runtime_error("File open failed"); } ~FileHandle() { if (file_) std::fclose(file_); } // Delete copy, allow move FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; FileHandle(FileHandle&& other) noexcept : file_(other.file_) { other.file_ = nullptr; } FileHandle& operator=(FileHandle&& other) noexcept { if (file_) std::fclose(file_); file_ = other.file_; other.file_ = nullptr; return *this; } std::FILE* get() const { return file_; } }; int main() { try { FileHandle fh("data.txt"); // opens file // use fh.get() } catch (...) {} // file auto-closed on scope exit } |
Smart pointers in depth (review + advanced):
- unique_ptr: Non-copyable, movable β for exclusive ownership.
- shared_ptr: Reference-counted β for shared, use make_shared for efficiency (single allocation).
- weak_ptr: Non-owning, use lock() to get shared_ptr.
- Custom deleters: unique_ptr<int, void(*)(int*)> p(new int[10], [](int* arr){ delete[] arr; });
Pitfalls: Circular references with shared_ptr β use weak_ptr.
3. Move Semantics & Perfect Forwarding
Move semantics: Transfer resources without copying β use && (rvalue ref).
In depth:
- std::move(x) casts x to rvalue β triggers move constructor/assignment.
- Move ctor: Class(Class&& other) noexcept β steal other’s resources, set other to null state.
Perfect forwarding: Forward args without losing type info (lvalue/rvalue, const).
Example β Forwarding Factory
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template <typename T, typename... Args> std::unique_ptr<T> make_unique_forward(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } class Widget { public: Widget(int& ref) { std::cout << "lvalue ref\n"; } Widget(int&& rval) { std::cout << "rvalue ref\n"; } }; int main() { int x = 42; auto w1 = make_unique_forward<Widget>(x); // lvalue auto w2 = make_unique_forward<Widget>(42); // rvalue } |
Pitfalls: Don’t use moved-from objects. Mark moves noexcept.
4. Type Traits & SFINAE
Type traits: <type_traits> β query properties at compile time.
Example:
|
0 1 2 3 4 5 6 7 |
static_assert(std::is_integral_v<int>, "Must be int"); if (std::is_floating_point_v<T>::value) { /* ... */ } |
SFINAE (Substitution Failure Is Not An Error): Enable/disable templates based on traits.
Example β Enable if arithmetic
|
0 1 2 3 4 5 6 7 |
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> T add(T a, T b) { return a + b; } |
In depth: SFINAE uses invalid substitutions to disable overloads silently.
5. CRTP β Curiously Recurring Template Pattern
CRTP: Derived class passed as template param to base β static polymorphism.
Example β Static Interface
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
template <typename Derived> class Base { public: void interface() { static_cast<Derived*>(this)->implementation(); } }; class MyClass : public Base<MyClass> { public: void implementation() { std::cout << "Implemented!\n"; } }; int main() { MyClass obj; obj.interface(); // Implemented! } |
Use cases: Mixins, compile-time polymorphism (faster than virtual).
Homework
- Implement Observer for a weather station notifying displays.
- Create a CRTP base for logging in derived classes.
- Use SFINAE to overload a function for pointers only.
You’ve mastered advanced C++ β proud of you! Questions? π
