Перейти к содержанию

Начало урока

Паттерны проектирования в C++

Как классические паттерны растворяются, обостряются или меняют форму в современном C++

Главная идея: Паттерны "банды четырёх" писались для языка без RAII, шаблонов, лямбд и семантики перемещения. В современном C++ (C++17/20) многие из них переформируются самим языком: ручная очистка становится RAII, динамическая диспетчеризация через 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 по духу - глотайте или логируйте ошибки, но никогда не выпускайте их из деструктора.

Продолжите этот урок на YourAIZone

В полном уроке — объяснение целиком, разобранные примеры, частые ошибки, тесты с мгновенной проверкой и ИИ-репетитор. Начните сразу как гость или создайте аккаунт, чтобы сохранять прогресс.

В полном уроке

  • Объяснение целиком
  • Разобранные примеры
  • Частые ошибки и как их избежать
  • Тестовые вопросы: 15
  • ИИ-репетитор, который отвечает на вопросы по теме
  • Отслеживание прогресса для ученика, родителей и учителя