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