Краткая справка по умным указателям в 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.
