Паттерны проектирования в C++
Как классические паттерны растворяются, обостряются или меняют форму в современном C++
virtual часто заменяется компиляционным CRTP, зависимости заголовков разрываются идиомой pimpl, а Strategy и Observer сжимаются до std::function и лямбды. Паттерн - это форма решения, а не иерархия классов, которую нужно копировать дословно.
- превратить любой ресурс в RAII-обёртку и написать универсальный scope guard;
- использовать CRTP для статического полиморфизма без накладных расходов и для миксинов;
- применять идиому pimpl для построения компиляционного барьера и стабильного ABI;
- реализовать Strategy и Observer через
std::function, лямбды иweak_ptr; - честно взвешивать компромиссы каждого выбора (накладные расходы, связанность, гибкость).
1. RAII как генератор паттернов (scope guard)
Вы уже используете RAII для памяти (unique_ptr), блокировок (std::lock_guard) и файлов (std::fstream). RAII - это не один паттерн, а фабрика паттернов: любой ресурс с шагами захвата и освобождения можно обернуть так, чтобы освобождение происходило автоматически в деструкторе на каждом пути выхода, включая раскрутку стека при исключении.
Когда готовой обёртки нет, scope guard обобщает RAII, чтобы выполнить произвольную очистку в конце области видимости. Он хранит вызываемый объект и вызывает его в деструкторе, если только вы заранее не отмените его через dismiss() (для пути успеха при 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_(); } // выполняется на любом пути выхода
ScopeGuard(const ScopeGuard&) = delete; // некопируемый
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
template <class F>
ScopeGuard<F> makeGuard(F f) { return ScopeGuard<F>(std::move(f)); }
Использование читается как ручной finally, но безопасно при исключениях и не требует try/catch:
FILE* f = std::fopen("data.bin", "rb");
auto guard = makeGuard([&]{ if (f) std::fclose(f); }); // всегда закрывает
// ... используем f; если здесь что-то бросит, guard всё равно закроет файл ...
Замечание про C++20 / библиотеку: та же идея стандартизирована в Library Fundamentals TS как std::experimental::scope_exit и присутствует в GSL как gsl::finally. Предпочитайте их собственной реализации, когда они доступны.
std::terminate. Лямбды очистки должны быть noexcept по духу - глотайте или логируйте ошибки, но никогда не выпускайте их из деструктора.