Умные указатели появились в стандарте С++11 и являются фундаментальной основой для написания безопасного кода с точки зрения утечек памяти в современном С++. Благодаря идиоме RAII (Resource Acquisition Is Initialization – получение ресурса есть инициализация), используемой в умных указателях, они позволяют эффективно работать с указателями на динамически выделенные области памяти. В этой статье вы узнаете об основных моментах при работе с этими полезными типами.
Содержание статьи 1. Общая информация 2. std::unique_ptr 3. std::shared_ptr 4. std::weak_ptr
Начнем с общих черт и описания в целом такого понятия, как умные указатели, а затем рассмотрим каждый вид умных указателей по отдельности. Рассмотрим их отличия, области применения, слабые и сильные места. Поехали!
1. Общая информация
- Определение умных указателей содержится в заголовочном файле <memory>
- Умные указатели назвали “умными” потому, что они хранят указатель на объект или некий ресурс, а также осуществляют владение этими данными. Они основаны на идиоме RAII.
- Указатели unique_ptr и shared_ptr имеют перегруженные оператора доступа * и ->, поэтому умные указатели могут быть разыменованы как и обычные “сырые” (raw) указатели.
- Для получения сырого указатели из объекта unique_ptr и shared_ptr используйте метод get().
- Метод .get() может быть полезен, когда вам необходимо передать в функцию сырой указатель для отслеживания состояния объекта (желательно не изменять объект, которым владеет умный указатель, через сырые указатели).
void useObject(MyType* pObj) { }
useObject(mySmartPtr.get());
- unique_ptr (начиная со стандарта C++11) и shared_ptr (со стандарта C++17) получили специализацию шаблона для управления массивами данных. Нововведение заключается во внедрении вызова оператора delete[] при удалении управляемого объекта). Это может быть полезно, когда вы получаете указатель на массив, например, из какой-либо библиотеки. Однако, если возможно, старайтесь использовать вместо массивов стандартные контейнеры такие как std::vector<T> или std::array<T>.
- Напоминаю! Не используйте в своем коде auto_ptr! Этот указатель имеет ряд критических проблем, отмечен устаревшим в стандарте C++11 и полностью удален в стандарте С++17. Взамен используйте unique_ptr. Для поиска таких мест можете воспользоваться настройкой modernize-replace-auto-ptr утилиты Clang Tidy для автоматизации рефакторинга.
- В стандартах C++17/20 отсутствует выведение аргументов шаблонов классов (class template argument deduction – CTAD) для умных указателей. Поэтому компилятор не в состоянии отличить указатель на массив от указателя на объекты не массивы созданные оператором new().
- Начиная со стандарта С++20 появились возможность применение атомарных указателей std::atomic<std::shared_ptr<T>> и std::atomic<std::weak_ptr<T>>. Также, в этом стандарты были отмечены устаревшими глобальные атомарные функции для умных указателей, доступных с С++11.
- C++20 добавляет различные функции конструирования *_for_overwrite, которые не принимают аргументов конструктора и используют инициализацию по умолчанию (аналогичную применению оператора new T). Это позволяет избежать ненужной инициализации в ситуациях, когда начальное значение никогда не считывается (например, чтение в буфер).
2. std::unique_ptr
std::unique_ptr это легковесный указатель монопольно владеющий ресурсом.
- std::unique_ptr уничтожает управляемый объект когда указатель покидает область видимости. Для удаления объекта и назначения (при необходимости) нового объекта в управление std::unique_ptr используйте метод reset().
- std::unique_ptr не может быть скопирован, но его можно переместить.
- Обычно, указатель имеет небольшой размер, сопоставимый с размером обычного сырого указателя на объект или размер двух указателей, если при создании умного указателя устанавливается пользовательская функция очистки памяти.
Создание
Наилучший и самый безопасный способ создания 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(...));
Пользовательские функции (операторы) удаления
Оператор или функция удаления это функциональный объект используемый для удаления ресурса. По умолчанию используются стандартные операторы delete или delete[].
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 является невалидным указателем после вызова std::move()
Дополнительная информация
- Метод reset() – сбрасывает указатель (владеемый объект удаляется)
- unique_ptr – полезен при реализации идиомы “pimpl” (pointer to implementation – указатель на реализацию).
- Уникальный указатель рассматривается в первую очередь как кандидат на возвращаемый объект из фабричных методов. Если уникальный указатель не подходит, тогда рассмотрите возможность применения shared_ptr (или weak_ptr).
3. std::shared_ptr
Общий (shared) указатель обеспечивает “распределенное” владение объектом. Множество shared_ptr могут указывать на один и тот же объект. Рядом с каждым объектом создается счетчик указателей, ссылающегося на него. Уничтожение объекта происходит в тот момент, когда уничтожается последний указатель на него (shared_ptr или weak_ptr).
- shared_ptr могут быть перемещены или скопированы
- shared_ptr имеет размер двух указателей: первый на контролируемый объект и второй на управляющий блок
- Управляющий блок содержит счетчик shared_ptr, счетчик weak_ptr, указатели на оператор удаления и аллокатор.
Создание
Рекомендуемый метод, как и с уникальными указателями, это использованием статического метода make_shared:
auto pObj = make_shared<MyType>(...)
make_shared выделяет место под контролируемый объект и рядом создает управляющий блок. Это позволяет компактнее использовать динамическую память.
Пользовательские операторы удаления
Указатель на оператор удаления хранится в управляющем блоке. При создании shared_ptr с помощью конструктора можно указать пользовательский оператор удаления.
void DelFn(MyTp* p) {
if (p) p->OnDelete();
delete p;
}
shared_ptr<MyTp> ptr(new MyTp(), DelFn);
- Оператор удаления обязан корректно обрабатывать нулевые указатели. Допускаются ситуации, когда оператор удаления может быть вызван в пустых указателях shared_ptr.
- Метод get_deleter() возвращает неконстантный указатель на оператор удаления.
Передача в функцию
Владение объектом в указателе shared_ptr не является монопольным, поэтому вы можете поделиться указателем на объект владения (для указателей unique_ptr не рекомендуется создавать несколько unique_ptr указателей на один и тот же объект). Счетчик shared указателей обновляется автоматически при создании и уничтожении объектов shared_ptr, но за этот функционал конечно приходится немного заплатить. Shared указатели можно перемещать методом std::move().
Для получения сырого указателя используйте метод get().
Дополнительная информация
- Изменение состояния счетчика указателей является атомарным, но доступ к указателю не является потоко-безопасным.
- Для создания shared_ptr указателей на *this используйте метод shared_from_this() и унаследуйте класс от std::enable_shared_from_this.
- Приведение типов указателей может выполняться функциями dynamic_pointer_cast, static_pointer_cast или reinterpret_pointer_cast.
- В виду не монопольности владения объектом, неаккуратное использование shared указателей может приводить к возникновению циклических зависимостей, когда несколько указателей ссылаются друг на друга и не могут уничтожить объект, что может приводить у утечке памяти
4. std::weak_ptr
Слабые (weak) указатели содержат слабую ссылку на объект, который управляется shared указателем. std::weak_ptr для доступа к указываемому объекту обязан быть преобразован в std::shared_ptr методом lock().
- Одним из примеров, где слабые указатели находят широкое применение это кэширование. Например, ресурсы выделяемые операционной системой всегда представляют собой слабые ссылки и перед использованием нужно убедится, что это ресурс всё ещё является доступным.
- Применение слабых указателей позволяет избежать циклической зависимости при применении shared указателей.
Создание
Слабые указатели создаются из shared_ptr, но перед их использованием их нужно конвертировать обратно в shared_ptr.
std::weak_ptr pWeak = pSharedPtr;
if (auto observe = pWeak.lock()) {
// Объект существует
} else {
// shared_ptr является пустым
}
При создании std::weak_ptr из std::shared_ptr происходит инкремент счетчика слабых указателей в управляющем блоке shared указателя. Это происходит даже в том случае, если все shared указатели, ссылающиеся на объект, были уничтожены, тогда управляющий блок существует до тех пора, пока существует хотя бы один std::weak_ptr, указываемый на него. Это может быть проблемой если управляющий блок был выделен в памяти рядом с управляемым объектом (например при использовании метода make_shared<T>()). Получается, что управляемый объект уничтожается, а вот управляющий блок остается висеть в памяти.
Дополнительная информация
- метод use_count() возвращает количество shared указателей, которые управляют одним и тем же объектом
- используйте expired() для проверки факта существования управляемого объекта
- слабые указатели не содержат операторов * и ->, поэтому единственный способ получить доступ к управляемому объекту – конвертировать weak_ptr в shared_ptr методом lock().
В качестве выводов
Эта статья познакомила или напомнила вам важную информацию об умных указателях в современном C++. Представленная здесь информация должна облегчить работу с основными типами умных указателей и идиомой RAII.
Пост является переводом статьи: https://www.cppstories.com/2021/smart-ptr-ref-card/