Design Patterns in C++
How classic patterns dissolve, sharpen, or change shape in modern C++
virtual dispatch is often replaced by compile-time CRTP, header dependencies are cut by the pimpl idiom, and Strategy and Observer shrink to a std::function and a lambda. A pattern is a solution shape, not a class hierarchy you must copy verbatim.
- turn any resource into an RAII wrapper and write a generic scope guard;
- use the CRTP for zero-overhead static polymorphism and mixins;
- apply the pimpl idiom to build a compilation firewall and a stable ABI;
- implement Strategy and Observer with
std::function, lambdas, andweak_ptr; - weigh the honest trade-offs of each choice (overhead, coupling, flexibility).
1. RAII as a pattern generator (scope guard)
You already use RAII for memory (unique_ptr), locks (std::lock_guard) and files (std::fstream). RAII is not one pattern; it is a factory of patterns: any resource with an acquire and a release step can be wrapped so that release happens automatically in the destructor, on every path, including exception unwinding.
When there is no ready-made wrapper, the scope guard generalises RAII to run arbitrary cleanup at end of scope. It stores a callable and invokes it in its destructor, unless you dismiss() it first (for the success path of a commit/rollback):
template <class F>
class ScopeGuard {
F cleanup_;
bool active_ = true;
public:
explicit ScopeGuard(F f) : cleanup_(std::move(f)) {}
void dismiss() noexcept { active_ = false; }
~ScopeGuard() { if (active_) cleanup_(); } // runs on every exit path
ScopeGuard(const ScopeGuard&) = delete; // non-copyable
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
template <class F>
ScopeGuard<F> makeGuard(F f) { return ScopeGuard<F>(std::move(f)); }
Usage reads like a manual finally, but is exception-safe and needs no try/catch:
FILE* f = std::fopen("data.bin", "rb");
auto guard = makeGuard([&]{ if (f) std::fclose(f); }); // always closes
// ... use f; if anything throws here, guard still closes the file ...
C++20 / library note: the same idea is standardised in the Library Fundamentals TS as std::experimental::scope_exit, and appears in the GSL as gsl::finally. Prefer those over rolling your own when available.
std::terminate. Cleanup lambdas should be noexcept in spirit - swallow or log errors, never let them escape a destructor.