Урок по треку C++ 6 мин

RAII и умные указатели

Время жизни объектов, unique_ptr, shared_ptr и weak_ptr изнутри, make_shared, циклы владения.

В C++ нет сборщика мусора, но есть правило, которое делает управление ресурсами почти автоматическим: RAII (Resource Acquisition Is Initialization). Ресурс захватывается в конструкторе объекта и освобождается в его деструкторе. Деструктор локального объекта вызывается при любом выходе из области видимости: обычном, через return, break или исключение.

void save(const std::string& path) {
    std::ofstream out(path);          // открыли файл
    std::lock_guard lock(mutex_);     // захватили мьютекс
    out << data_;
}                                     // закрыли файл и отпустили мьютекс автоматически

RAII работает для памяти, файлов, сокетов, мьютексов, транзакций. Объекты уничтожаются в порядке, обратном созданию: и локальные переменные, и члены класса (члены в порядке, обратном объявлению).

Почему не new и delete

Widget* w = new Widget;
process(w);      // если тут исключение, delete не выполнится: утечка
delete w;

Ручное управление ломается на исключениях, ранних return и при путанице, кто владеет объектом. Правило современного C++: голые new и delete в прикладном коде не пишем. Владение выражаем типом.

std::unique_ptr: единоличное владение

auto p = std::make_unique<Widget>(42);
auto q = std::move(p);     // владение передано, p теперь пустой
// auto r = q;             // ошибка компиляции: копировать нельзя
  • Владелец ровно один, копирование запрещено, перемещение разрешено.
  • С обычным удалителем занимает столько же, сколько сырой указатель, и не добавляет накладных расходов.
  • Есть версия для массивов std::unique_ptr<T[]> (вызывает delete[]) и пользовательские удалители: std::unique_ptr<FILE, decltype(&std::fclose)>.
  • Фабрики возвращают unique_ptr, а функция, принимающая владение, берёт его по значению.

std::shared_ptr: совместное владение

auto a = std::make_shared<Widget>();
auto b = a;                // счётчик ссылок 2

Устройство: сам shared_ptr хранит два указателя: на объект и на управляющий блок. В блоке счётчик сильных ссылок, счётчик слабых ссылок, удалитель и аллокатор. Объект уничтожается, когда сильный счётчик падает до нуля, блок освобождается, когда умрут и все weak_ptr.

  • std::make_shared выделяет объект и блок одним куском памяти: меньше аллокаций и лучше локальность. Минус: память объекта не вернётся, пока жив хоть один weak_ptr.
  • Изменение счётчиков атомарно, поэтому копировать разные экземпляры shared_ptr на один объект из разных потоков безопасно. Но доступ к самому объекту не синхронизирован, а одновременная запись в один и тот же экземпляр shared_ptr это гонка (для этого есть std::atomic<std::shared_ptr<T>> в C++20).
  • Копирование shared_ptr не бесплатно: атомарный инкремент. Передавайте его по const&, если функция не забирает владение, а ещё лучше передавайте T&.

std::weak_ptr: наблюдатель

weak_ptr не продлевает жизнь объекта. Чтобы воспользоваться объектом, его превращают в shared_ptr через lock(): если объект уже умер, вернётся пустой указатель.

Главное применение: разрыв циклов. Если родитель держит детей через shared_ptr, а дети родителя тоже через shared_ptr, счётчики никогда не упадут до нуля, и вся структура утечёт. Обратную ссылку делают weak_ptr (или сырым указателем, если время жизни гарантировано). Ещё применение: кеши и подписчики.

enable_shared_from_this

Если объекту, уже управляемому shared_ptr, нужно выдать указатель на себя, нельзя писать std::shared_ptr<T>(this): появится второй управляющий блок и двойное удаление. Наследуйтесь от std::enable_shared_from_this<T> и вызывайте shared_from_this().

Частые ошибки

  • delete для памяти из new[] и наоборот: UB.
  • Два shared_ptr, созданных из одного сырого указателя.
  • Циклы из shared_ptr и «утечки» в долгоживущих кешах.
  • shared_ptr везде «на всякий случай»: владение размывается, растут накладные расходы. По умолчанию выбирайте unique_ptr.
  • Полиморфное удаление через указатель на базу без виртуального деструктора.

Что спросят на собесе

  • Что такое RAII и почему это главный механизм C++.
  • Разница unique_ptr, shared_ptr, weak_ptr, когда что выбрать.
  • Как устроен shared_ptr и чем make_shared лучше shared_ptr(new T).
  • Потокобезопасен ли shared_ptr.
  • Как появляются циклы владения и как их разорвать.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.