Краткая справка по умным указателям в C++

Добавлено 31 декабря 2022 в 14:35

Умные указатели, доступные начиная с C++11, являются важной основой для написания безопасного кода на современном C++. Благодаря RAII (Resource Acquisition Is Initialization) они позволяют вам работать с указателями для эффективного выделения памяти или других управляемых объектов.

Этот пост покажет вам основные моменты работы с этими удобными типами.

Краткая справка по умным указателям в C++

Содержание

Основы

  • Умные указатели расположены в заголовке <memory>.
  • Умные указатели являются «умными», потому что они содержат указатель на объект/ресурс, а также знают владельца указателя. Их работа основана на паттерне RAII.
  • unique_ptr и shared_ptr имеют перегруженные операторы доступа * и ->, поэтому умные указатели можно разыменовывать как обычные простые указатели.
  • Используйте .get() (для unique_ptr и shared_ptr) для доступа к базовому простому указателю.
  • .get() полезен, когда вы хотите передать указатель в функцию, чтобы «наблюдать» за управляемым объектом.
    void useObject(MyType* pObj) { }
    useObject(mySmartPtr.get());
  • unique_ptr (начиная с C++11) и shared_ptr (начиная с C++17) имеют шаблонную специализацию для массивов (при очистке будет вызываться delete[]). Это может быть полезно, когда вы получаете указатель на массив из какой-то сторонней библиотеки или устаревшей системы. Тем не менее, если есть возможность, лучше использовать стандартные контейнеры вроде std::vector или std::array.
  • Напоминание: не используйте auto_ptr! Он устарел, начиная с C++11, и удален в C++17. Замените его на unique_ptr.
  • Вы можете попробовать использовать команду modernize-replace-auto-ptr от Clang Tidy для автоматизации рефакторинга.
  • В C++17/C++20 для умных указателей нет вывода аргумента шаблона класса (CTAD, class template argument deduction). Компилятор не может отличить указатель, полученный из массива, от «не-массивных» форм new().
  • Начиная с C++20 существуют атомарные умные указатели std::atomic<std::shared_ptr<T>> и std::atomic<std::weak_ptr<T>>. C++20 также отмечает устаревшими глобальные атомарные функции для умных указателей, доступные начиная с C++11.
  • В C++20 добавлены различные функции создания *_for_overwrite, которые не принимают аргументов конструктора и используют инициализацию по умолчанию (эквивалентно new T). Это позволяет избежать ненужной инициализации в ситуациях, когда начальное значение никогда не читается (например, чтение в буфер).

std::unique_ptr

Это легковесный умный указатель, обладающий уникальным владением управляемым объектом.

  • unique_ptr уничтожает базовый объект, когда он выходит за пределы области видимости; когда вызывается его функция-член reset(), или когда ему выполняется присваивание с новым указателем/объектом.
  • unique_ptr можно перемещать, но нельзя копировать.
  • Обычно его размер – это размер одного простого указателя (в случае без пользовательского делитера (deleter), или двух указателей, когда требуется указатель для делитера).

Создание

unique_ptr лучше всего создавать с auto и std::make_unique:

auto pObj = make_unique<MyType>(...);

или с явным new:

unique_ptr<MyType> pObject(new MyType(...));

В приведенном выше случае тип встречается дважды, и вам нужно использовать явный new, что не считается современным подходом. Однако этот код может быть полезен при создании полиморфного объекта:

unique_ptr<Base> pObject(new MyDerived(...));

Пользовательские делитеры

Делитер (deleter) – это вызываемый объект, используемый для удаления ресурса. По умолчанию используется delete или delete[]. Тип делитера является частью типа unique_ptr.

struct DelFn {  
  void operator()(MyTy* p) {
    p->SpecialDelete(); 
    delete p;
  }
};

using my_ptr = unique_ptr<MyTy, DelFn>;
  • Делитер не вызывается, когда указатель равен нулю
  • get_deleter() может возвращать неконстантную ссылку на делитер, поэтому ее можно использовать для его замены.

Передача в функции

unique_ptr является только перемещаемым, его следует передавать с помощью std::move, чтобы явно выразить передачу владения:

auto pObj = make_unique<MyType>(...);

func(std::move(pObj));

// pObj недействителен после вызова!

Прочее

  • reset() – сбрасывает указатель (удаляет старый).
  • unique_ptr также полезен в реализации идиомы «pimpl».
  • unique_ptr обычно является первым кандидатом на возврат из фабричных функций. Если фабрики становятся более сложными (например, при добавлении кешей), вы можете использовать shared_ptr (или weak_ptr).

std::shared_ptr

Несколько shared_ptr могут указывать на один и тот же объект, деля владение им. Когда последний shared_ptr выходит за пределы области видимости, управляемый объект удаляется. Это реализуется с помощью подсчета ссылок.

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

Создание

Рекомендуемый метод – через std::make_shared():

auto pObj = make_shared<MyType>(...)

make_shared обычно размещает управляющий блок рядом с объектом, что обеспечивает лучшую локализацию памяти.

Пользовательские делитеры

Делитер хранится в управляющем блоке и может быть передан во время создания (но не с помощью make_shared()). Делитер не является частью типа и может быть чем угодно вызываемым.

void DelFn(MyTp* p) {
    if (p) p->OnDelete(); 
    delete p;
}

shared_ptr<MyTp> ptr(new MyTp(), DelFn);
  • Делитер должен справляться с нулевыми значениями указателя. Делитер может быть вызван, когда указатель пуст.
  • get_deleter() (функция, не являющаяся членом) возвращает неконстантный указатель на делитер.

Передача в функции

Чтобы поделиться владением, передайте общий указатель по значению. Счетчик ссылок обновляется атомарно, поэтому вам необходимо учитывать дополнительные затраты на синхронизацию. Для передачи владения также можно использовать std::move.

Чтобы наблюдать за объектом, используйте .get().

Прочее

  • Доступ к счетчику ссылок является атомарным, но доступ к указателю не является потокобезопасным.
  • Используйте shared_from_this(), чтобы вернуть shared_ptr на *this. Класс должен быть производным от std::enable_shared_from_this.
  • Приведение типов указателей можно выполнить с помощью dynamic_pointer_cast, static_pointer_cast или reinterpret_pointer_cast.
  • shared_ptr может создавать циклические зависимости и утечки памяти, когда два указателя указывают друг на друга.

std::weak_ptr

Это умный указатель, не являющийся владельцем, который содержит «слабую» ссылку на объект, управляемый std::shared_ptr. Для доступа к объекту, на который указывает ссылка, он должен быть преобразован в std::shared_ptr через метод lock().

  • Одним из примеров использования слабых указателей является кэширование. Такая система распространяет только слабые указатели, и перед любым использованием клиент должен проверить, жив ли ресурс.
  • Слабый указатель также используется для прерывания циклов в общих указателях.

Создание

Слабый указатель создается из shared_ptr, но перед его использованием его нужно снова преобразовать в shared_ptr.

weak_ptr pWeak = pSharedPtr;

if (auto observe = pWeak.lock()) {
    // объект жив
} else {
   // shared_ptr был удален
}

Слабый указатель, созданный из общего указателя, увеличит «счетчик слабых ссылок», который хранится в управляющем блоке общего указателя. Даже если все общие указатели (ссылающиеся на один объект) мертвы, но один слабый указатель имеет слабую ссылку (на этот объект), управляющий блок всё еще может присутствовать в памяти. Это может быть проблемой, когда память под управляющий блок выделяется вместе с объектом (например, при использовании make_shared). В этом случае деструктор объекта вызывается, но память не освобождается.

Прочее

  • use_count() возвращает количество общих указателей, совместно использующих один и тот же управляемый объект.
  • Чтобы проверить, существует ли еще управляемый объект, используйте expired().
  • Слабый указатель не имеет перегруженных операторов * и ->, поэтому вы не можете разыменовать базовый указатель до преобразования в shared_ptr (через lock()).

Резюме

В данной статье содержится важная информация об умных указателях в современном C++. С помощью этой краткой справки будет проще работать с основными типами для указателей и RAII.

Теги

C++ / CppCustom deleter / Пользовательский удалительstd::shared_ptrstd::unique_ptrstd::weak_ptrПрограммированиеУмные указатели

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.