Что такое RAII

RAII (Resource Acquisition Is Initialization) значит, что при получении какого-либо ресурса, его инициализируют в конструкторе, а, поработав с ним в функции - корректно освобождают в деструкторе. Даже если в коде бросится исключение, то RAII-объект должен корректно уничтожить принадлежащий ему объект.

Существует три типа гарантии безопасности исключений:

  1. Базовая гарантия - при возникновении любого исключения в некотором методе, состояние программы должно оставаться согласованным. Это означает, не только отсутствие утечек ресурсов, но и сохранение инвариантов класса.
  2. Строгая гарантия - если при выполнении операции возникает исключение, то это не должно оказать какого-либо влияния на состояние приложения. Другими словами, строгая гарантия исключений обеспечивает транзакционность операций.
  3. Гарантия отсутствия исключений - ни при каких обстоятельствах функция не будет генерировать исключения.

std::shared_ptr

Осуществляет подсчет ссылок. Когда количество ссылок обнулится, хранимый объект уничтожится с помощью delete, delete[] или переданного в конструкторе deleter'а.

Копируемый. Потокобезопасный на методы своего класса при вызовах к разным экземплярам shared_ptr (отсюда следует, что счетчик ссылок атомарен), но может случиться data race при доступе к одному и тому же экземпляру shared_ptr.

Из-за того, что каждое копирование shared_ptr - дорогое удовольствие, лучше пользоваться его передачей по ссылке, где это возможно.

В конструкторе тип данных передаваемого указателя должен быть complete-типом.

Частая ошибка:

T *p = new T(); std::shared_ptr<T> p1(p); std::shared_ptr<T> p2(p);

Такой код некорректен, так как у p1 и p2 разные счётчики ссылок, поэтому объект *p удалится дважды. Чтобы этого не происходило, не нужно оборачивать один сырой указатель в shared_ptr дважды - есть конструкторы копирования.

Aliasing constructor

Иногда возникает желание ссылаться с помощью shared_ptr на объект и его мемберов. Наивное решение:

struct wheel {}; struct vehicle { std::array<std::shared_ptr<wheel>, 4> wheels; };

Проблема такого подхода в том, что при удалении vehicle, wheel остаются живы, пока на них кто-то ссылается.

Можем захотеть такое поведение: пока кто-то ссылается на составную часть объекта, основной объект жив. Для этого можно использовать для них общий счётчик ссылок.

struct wheel {}; struct vehicle { std::array<wheel, 4> wheels; }; void foo() { std::shared_ptr<vehicle> v(new vehicle()); std::shared_ptr<std::array<wheel, 4>> w(v, &v->wheels); store_for_later(w); } // vehicle is still alive

В таком случае оба указателя отвечают за удаление объекта vehicle (в зависимости от того, какой из указателей будет разрушен раньше), поэтому deleter у них общий, кроме того в управляющем блоке хранится указатель на исходный объект, чтобы передать его в deleter.

std::make_shared - зачем?

Потенциально одна аллокация вместо двух + cache-friendly - control block и объект, которым владеем, лежат в одном куске памяти.

Кроме экономии аллокаций, make_shared избавляет нас от необходимости следить за исключениями в new. Пример кода:

bar(std::shared_ptr<mytype>(new mytype(1, 2, 3)), std::shared_ptr<mytype>(new mytype(4, 5, 6)));

Так как порядок выполнения не задан, сначала может вызваться первый new, затем второй, а потом только конструкторы shared_ptr. В таком случае, если второй new кинет исключение, то первый объект не удалится. make_shared позволяет избежать этой ошибки.

std::enable_shared_from_this - зачем?

Мы уже сказали, что следующий код некорректен:

T *p = new T(); std::shared_ptr<T> p1(p); std::shared_ptr<T> p2(p);

Точно такой же ошибкой, но в другой обертке, может стать и возврат из метода класса shared_ptr(this), если текущий объект уже находится под наблюдением shared_ptr.

Пример хорошего и плохого кода:

#include <memory> #include <iostream> struct Good : std::enable_shared_from_this<Good> { std::shared_ptr<Good> getptr() { return shared_from_this(); } }; struct Bad { std::shared_ptr<Bad> getptr() { return std::shared_ptr<Bad>(this); } ~Bad() { std::cout << "Bad::~Bad() called\n"; } }; int main() { // Good: the two shared_ptr's share the same object std::shared_ptr<Good> gp1(new Good); std::shared_ptr<Good> gp2 = gp1->getptr(); // Bad, each shared_ptr thinks it's the only owner of the object std::shared_ptr<Bad> bp1(new Bad); std::shared_ptr<Bad> bp2 = bp1->getptr(); } // UB: double-delete of Bad

std::weak_ptr

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

Отсюда способ его получения - либо конструкторы от других weak_ptr, либо конструктор от shared_ptr.

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

Имея экземпляр weak_ptr можно получить shared_ptr на ссылаемый объект с помощью метода weak_ptr::lock(), который вернет экземпляр shared_ptr, который ссылается на исходный объект (сделав +1 к счетчику, разумеется), если количество "сильных" ссылок еще не обнулено, и объект не удален, либо вернет пустой shared_ptr.

Данный указатель является потокобезопасным в том же смысле, как shared_ptr.

Использование weak_ptr необходимо, в частности, чтобы иметь два объекта, ссылающихся на друг друга умными указателями, но при этом не вызывающих утечку памяти.

Приведение shared_ptr к указателям другого типа

В стандартной библиотеке реализованы все 4 вида кастов (static, dynamic, const, reinterpret) для shared_ptr - они создают новый инстанс shared_ptr, который хранит указатель, приведённый соответствующим кастом, и разделяет владение (счётчик ссылок) с исходным shared_ptr (привет, aliasing constructor).

Внутри это выглядит так, на примере static_cast:

template <class T, class U> std::shared_ptr<T> static_pointer_cast(const std::shared_ptr<U>& r) noexcept { auto p = static_cast<typename std::shared_ptr<T>::element_type*>(r.get()); return std::shared_ptr<T>(r, p); }

dynamic_cast выглядит немного иначе - в случае "неудачного каста" (который возвращает nullptr) вернется пустой shared_ptr.

std::unique_ptr

До внедрения в стандарт move-семантики (до С++11) разработчики могли хотеть в языке указатель, следовавший концепции RAII, но не разделяющий владение. Так появился auto_ptr.

С приходом C++11 его пришлось пометить как deprecated, поскольку его оператор присваивания работал ровным счетом как оператор перемещения сейчас, что могло приводить к непониманию и абсолютно не вписывалось в новые фишки стандарта. Так появился unique_ptr.

unique_ptr не хранит счетчик ссылок (а является полным владельцем переданного ему указателя), конструктор копирования и оператор присваивания у него запрещены, зато его можно мувать.

В отличие от shared_ptr этому умному указателю не нужны дополнительные функции для кастования (опять же, потому что он является полным владельцем своего объекта), но ему так же можно настраивать deleter и вызывать функцию make_unique(), подобную таковой у первого указателя.


Заимствования:

cpp-notes/16_smart_pointers.md at master · lejabque/cpp-notes (github.com)

std::enable_shared_from_this - cppreference.com