Умные указатели
std::unique_ptr
Мотивирующий пример
Когда мы писали контейнеры, часто мы встречали такой код:
container* create_container() {
container* c = new container();
try {
fill(*c);
} catch (...) {
delete c;
throw;
}
return c;
}
Если функция fill
бросала исключение, нам нужно было удалить память, выделенную по указателю, и пробросить исключение. Проблема такого кода, что он трудно масштабируется на несколько объектов, которые требуется удалять.
Для решения этой проблемы в C++11 появились умные указатели, которые являются RAII-обёртками над обычными указателями. Деструкторы таких указателей удаляют объект, на который они ссылаются. Например, с std::unique_ptr
код выглядел бы так:
std::unique_ptr<container> create_container() {
std::unique_ptr<container> c(new container());
fill(*c);
return c;
}
Реализация
unique_ptr
- самый простой из умных указателей. Внутри он хранит указатель T* ptr
и вызывает delete
от него в деструкторе.
template <typename T>
struct unique_ptr {
unique_ptr()
: ptr(nullptr) {}
unique_ptr(T* ptr)
: ptr(ptr) {}
~unique_ptr() {
delete ptr;
}
private:
T* ptr;
}
unique_ptr
имеет операторы *
и ->
, поэтому им можно пользоваться как обычным указателем.
T& operator*() const {
return *ptr;
}
T* operator->() const noexcept {
return ptr;
}
Помимо этого, у него есть функция get()
, возвращающая указатель, который лежит в нём.
T* get() const {
return ptr;
}
Так же есть release()
- зануляет ptr
, хранящийся внутри, а старое значение возвращает наружу. release()
обычно используется, если в некоторую функцию нужно передать указатель, который хранит unique_ptr
, при этом известно, что функция самостоятельно удалит объект.
T* release() {
T* tmp = ptr;
ptr = nullptr;
return tmp;
}
reset(p)
- заменяет указатель ptr
на p
и делает delete
от старого указателя.
void reset(T* p) {
delete ptr;
ptr = p;
}
В названии unique_ptr
заключается его основное свойство - он хранит "уникальный" указатель на объект (а точнее, он единственный ответственен за его удаление). Поэтому оператор присваивания и конструктор копирования у него запрещены, но есть move constructor
и move assignment
:
unique_ptr(unique_ptr const& other) = delete;
unique_ptr& operator=(unique_ptr const& other) = delete;
unique_ptr& operator=(unique_ptr&& other) noexcept {
reset(other.release());
return *this;
}
unique_ptr(unique_ptr&& other) noexcept
: ptr(other.release()) {}
Неявные приведения
Для unique_ptr
реализована логика приведений аналогичная той, что есть у обычных указателей. unique_ptr<D>
можно привести к unique_ptr<B>
тогда и только тогда, когда разрешено приведение D*
к B*
.
template <typename T>
struct unique_ptr {
template <typename U, typename = typename std::enable_if_t<is_convertible_v<U*, T*>>>
unique_ptr(unique_ptr<U>&& other);
};
Настраиваемый deleter
До этого мы говорили, что деструктор у unique_ptr
вызывает delete
от указателя. В общем случае это не всегда верно. Например, память под объект могла быть выделена кастомным аллокатором или malloc
, new[]
. Для такого использования std::unique_ptr
поддерживает пользовательский deleter
. По умолчанию в качестве deleter
используется класс std::default_delete
, который вызывает delete
на указателе.
Реализация unique_ptr
с deleter
могла бы выглядеть так:
template<typename T, typename D = default_delete<T>>
struct unique_ptr {
using pointer = typename D::pointer;
unique_ptr(pointer);
unique_ptr(pointer, D);
~unique_ptr() {
deleter(ptr);
}
private:
pointer ptr;
D deleter;
}
К сожалению, в реальности реализация сложнее из-за следующих причин:
- Шаблонный параметр
D
может быть не только самимdeleter
, но и ссылкой на него, поэтому ссылку сD
нужно снимать. Deleter
не обязан содержатьtypedef pointer
. Если он отсутствует,unique_ptr
должен использоватьT*
вместо него. Это можно сделать SFINAE-проверкой.- Часто
deleter
это пустой объект, поэтому, чтобы не увеличивать размерunique_ptr
, пользуются empty-base оптимизацией.
При перемещении unique_ptr
, перемещается и его deleter
. Отдельный случай - это неявные приведения, так как unique_ptr
от разных типов могут иметь разные deleter
. При использовании кастомных deleter
, unique_ptr<T1, D1>
приводится к unique_ptr<T2, D2>
, если выполнено одно из двух условий:
D2
- ссылка,D1
совпадает сD2
D2
- не ссылка,D1
приводится кD2
unique_ptr для массивов
Для массивов у unique_ptr
есть отдельная специализация:
template <typename T, typename D = default_delete<T>>
struct unique_ptr;
template <typename E, typename D>
struct unique_ptr<E[], D> {
E& operator[](size_t index) const;
};
Отличается она следующим:
-
Наличием
operator[]
-
Хранит внутри
E*
, а неT*
-
Использует более сильные правила для неявных преобразований, так как приведение указателей на массивы зависит не только от типов, но и от размеров массивов.
default_delete
можно использовать и для массивов, так как у него тоже есть специализация, которая делает delete[]
вместо delete
.
std::shared_ptr
Термин "владение"
Владением называют ответственность за удаление объекта. Например, unique_ptr
ответственен за удаление объекта, на который он ссылается, соответственно говорят, что unique_ptr
владеет объектом, на который он ссылается. Функция reset(p)
передаёт владение объектом, а функция release()
наоборот, забирает владение объектом.
В некоторых случаях у объекта может быть несколько владельцев. Это называется разделяемым владением и работает следующим образом: за удаление объекта ответственен последний владелец. Для умных указателей существует два способа реализации такого владения: подсчёт ссылок и провязка всех владельцев в двусвязный список. std::shared_ptr
использует подсчёт ссылок. Указатель, использующий провязку владельцев в двусвязный список, в стандартной библиотеке отсутствует, но часто его называют linked_ptr
.
Пример применения
shared_ptr
очень похож на логику, которая нужна для реализации оптимизации Copy-on-Write.
shared_ptr<data const>
можно использовать, если не нужно модифицировать значение.
Реализация
template<typename T>
struct shared_ptr {
shared_ptr();
explicit shared_ptr(T* ptr);
shared_ptr(shared_ptr const& other);
shared_ptr& operator=(shared_ptr const& other);
};
Как и unique_ptr
, shared_ptr
поддерживает операции operator*
, operator->
, reset
, get
. Операции release
у shared_ptr
нет, потому что могут быть другие shared_ptr
, ссылающиеся на этот объект.
shared_ptr
внутри себя хранит указатель на ссылаемый объект и указатель на счётчик ссылок. Поскольку счётчик ссылок у всех shared_ptr
, ссылающихся на один и тот же объект, общий, то память под него аллоцируется динамически.
template<typename T>
struct shared_ptr {
shared_ptr()
: ptr(nullptr),
ref_counter(nullptr) {}
shared_ptr(T* ptr)
: ptr(ptr) {
if (ptr == nullptr) {
return;
}
try {
ref_counter = new size_t(1);
} catch (...) {
delete ptr;
throw;
}
}
shared_ptr(shared_ptr const& other)
: ptr(other.ptr),
ref_counter(other.ref_counter) {
if (ptr == nullptr) {
return;
}
++ref_counter;
}
~shared_ptr() {
if (ptr == nullptr) {
return;
}
if (--*ref_counter == 0) {
delete ref_counter;
delete ptr;
}
}
private:
T* ptr;
size_t* ref_counter;
}
Если shared_ptr
не ссылается ни на один объект, то ptr == nullptr
, ref_counter == nullptr
.
Если shared_ptr
ссылается на некоторый объект, то ptr != nullptr
, ref_counter != nullptr
, *ref_counter
равен числу shared_ptr
, ссылающихся на объект *ptr
.
Проблема разделения счётчика ссылок
Посмотрим на следующий код:
T* p = new T();
std::shared_ptr<T> p1(p);
std::shared_ptr<T> p2(p);
Такой код некорректен, так как у p1
и p2
разные счётчики ссылок, поэтому объект *p
удалится дважды. Такая ситуация называется разделением (split) счётчика ссылок. Разделение счётчика ссылок - ошибка, поэтому в корректных программах оно возникать не должно.
Чтобы не происходило разделение счётчика ссылок, не нужно оборачивать один сырой указатель в shared_ptr
дважды. Использование std::make_shared
или использование std::enable_shared_from_this
также позволяет избежать разделения счётчика ссылок.
Настраиваемый deleter
Если в shared_ptr
, например, похранить массив (или любой объект, созданный не с помощью new
), то, по аналогии с unique_ptr
, нам нужен deleter
. Он может храниться, например, в памяти рядом со счётчиком ссылок.
Aliasing constructor
Иногда возникает желание ссылаться с помощью shared_ptr
на объект и его мемберов.
Наивное решение такого:
struct wheel {};
struct vehicle {
std::array<std::shared_ptr<wheel>, 4> wheels;
};
Проблема такого подхода в том, что при удалении vehicle
, wheel
остаются живы, пока на них кто-то ссылается.
Можем захотеть такое поведение: пока кто-то ссылается на составную часть объекта, основной объект жив. Для этого можно хранить для них общий счётчик ссылок.
struct vehicle {
std::array<wheel, 4> wheels;
}
std::shared_ptr<vehicle> v(new vehicle);
std::shared_ptr<wheel> w(v, &v->wheels[2]);
В таком случае оба указателя отвечают за удаление объекта vehicle
(в зависимости от того, какой из указателей раньше будет разрушен), поэтому deleter
у них общий, кроме того в управляющем блоке хранится ptr
на исходный объект, чтобы передать его в deleter
.
Одно из применений такого конструктора - указатели в дереве. Пока будет существовать хотя бы один указатель на элемент дерева, всё дерево будет продолжать существовать.
std::make_shared
Рассмотрим следующий пример кода:
shared_ptr<mytype> sp(new mytype(1, 2, 3));
Такой код аллоцирует два объекта в куче - один при создании new mytype
, другой - счётчик ссылок внутри shared_ptr
. Таким образом, такое использование удваивает количество аллокаций, что не очень хорошо.
Для решения этой проблемы существует специальная функция make_shared
:
template<typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args);
make_shared
создаёт объект типа T
и возвращает shared_ptr
на него. При этом make_shared
делает одну аллокацию памяти, то есть и объект, и счётчик ссылок выделяются в одном блоке.
Кроме экономии аллокаций, 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::weak_ptr
weak_ptr
- парный указатель к shared_ptr
. Он нужен, чтобы ссылаться на объект, но не мешать ему удаляться. Один из способов его реализации - хранить отдельно счётчики сильных и слабых ссылок. Для удобства в проверках счётчик слабых ссылок не нулевой, если есть хотя бы одна сильная ссылка (например, можно для каждой сильной ссылки увеличивать счётчик слабых).
Пример применения weak_ptr
- кэши.
shared_ptr<widget> get_widget(int id) {
static map<int, weak_ptr<widget>> cache;
auto sp = cache[id].lock();
if (!sp) {
cache[id] = sp = load_widget(id);
}
return sp;
}
В этом примере weak_ptr
не мешают удаляться виджетам. Методlock
у weak_ptr
создаёт новый shared_ptr
на объект, если объект ещё не удалён, иначе возвращает пустой shared_ptr
.
Проблема примера выше в том, что из cache
никогда не удаляются вхождения, даже если объекты удалены. При этом, так как у нас будут висеть weak_ptr
, не будут удаляться control_block
'и (счётчики ссылок в памяти).
Это можно решить, например, кастомным делитером, который будет удалять из мапы. Проблема реализации, приведённой по ссылке, в том, что там не применяется make_shared
, так как у объекта кастомный deleter
. Это можно решить с помощью обёртки над объектом, деструктор которой будет удалять объект из мапы. Тогда можно применить aliasing constructor, чтобы получать указатели не на обёртку, а на сам объект - member обёртки.
Так же стоит упомянуть, что использование make_shared
с weak_ptr
может быть неэффективным. Так как make_shared
создаёт объект и счётчик ссылок одним блоком памяти, то даже когда счётчик сильных ссылок обнулится, control_block
продолжит существовать, пока жив хотя бы один weak_ptr
. Таким образом память, выделенная под объект, не будет освобождена до удаления control block
(хотя деструктор объекта вызовется).
std::enable_shared_from_this
Если хотим вернуть shared_ptr
от this
, то нельзя сделать это просто как std::shared_ptr(this)
, так как на каждый shared_ptr
будут разные control_block
'и. В стандартной библиотеке для этого есть класс std::enable_shared_from_this
:
struct mytype : std::enable_shared_from_this<mytype> {
std::shared_ptr<mytype> bar() {
return shared_from_this();
}
}
Реализация enable_shared_from_this
хранит внутри weak_ptr
, от которого создаётся shared_ptr
при вызове shared_from_this()
.
Конструктор shared_ptr(T*)
использует SFINAE-проверки для определения наследования от enable_shared_from_this
, поэтому наследоваться нужно публично.
shared_from_this()
можно вызывать только от previously shared объектов, поэтому нельзя вызывать её внутри конструктора и деструктора. Конструктор обычного shared_ptr
для объектов, которые наследуются от enable_shared_from_this
, "включает" возможность создавать shared_from_this. При вызове std::shared_ptr(U*)
для указателя на тип U, у которого есть unambiguous and accessible базовый класс, который является специализацией std::enable_shared_from_this
, выполняется следующее:
if (ptr != nullptr && ptr->weak_this.expired())
ptr->weak_this = std::shared_ptr<std::remove_cv_t<U>>(*this,
const_cast<std::remove_cv_t<U>*>(ptr));
Поэтому, если от объекта не создавался shared_ptr
, вызов функции shared_from_this()
начиная с C++17 бросит исключение std::bad_weak_ptr
, а до C++17 ведёт к UB.
При этом, даже если объект наследуется от enable_shared_from_this
, создание нескольких shared_ptr
от указателя на один объект не гарантирует, что это будет эквивалентно вызовам shared_from_this()
и является UB.
Приведения shared_ptr к указателям
В стандартной библиотеке реализованы все 4 вида кастов (static
, dynamic
, const
, reinterpret
) для shared_ptr
- они создают новый инстант shared_ptr
, который хранит указатель приведённый соответствующим кастом и разделяет владение (счётчик ссылок) с исходным shared_ptr
.
Внутри это выглядит так (на примере 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
.