Краткая справка по умным указателям в C++
Умные указатели, доступные начиная с C++11, являются важной основой для написания безопасного кода на современном C++. Благодаря RAII (Resource Acquisition Is Initialization) они позволяют вам работать с указателями для эффективного выделения памяти или других управляемых объектов.
Этот пост покажет вам основные моменты работы с этими удобными типами.
Содержание
Основы
- Умные указатели расположены в заголовке
<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.