Умный указатель

Умный указатель (англ. smart pointer) — идиома косвенного обращения к памяти, которая широко используется при программировании на языках высокого уровня: C++, Rust и так далее. Как правило, реализуется в виде специализированного класса (обычно — параметризованного), имитирующего интерфейс обычного указателя и добавляющего необходимую новую функциональность (например — проверку границ при доступе или очистку памяти)[1].

Как правило, основной целью задействования умных указателей является инкапсуляция работы с динамической памятью таким образом, чтобы свойства и поведение умных указателей имитировали свойства и поведение обычных указателей. При этом на них возлагается обязанность своевременного и аккуратного высвобождения выделенных ресурсов, что упрощает разработку кода и процесс отладки, исключая утечки памяти и возникновение висячих ссылок[2].

Указатели совместного владения (с подсчётом ссылок)

Такие обычно используются с объектами, имеющими специальные операции «увеличить число ссылок» (AddRef() в COM) и «уменьшить число ссылок» (Release() в COM). Чаще всего такие объекты унаследованы от специального класса или интерфейса (например, IUnknown в COM).

При появлении новой ссылки на объект вызывается операция «увеличить число ссылок», а при уничтожении — «уменьшить число ссылок». Если в результате операции «уменьшить число ссылок» число ссылок на объект становится равным нулю, то объект удаляется.

Такая методика называется автоматическим подсчётом ссылок. Она согласует число указателей, хранящих адрес объекта, с числом ссылок, хранящимся в объекте, а при достижении этим числом нулевого значения приводит к удалению объекта. Её преимуществами являются относительно высокие надёжность, быстродействие и простота реализации в C++. Недостатком является усложнение использования в случае возникновения циклических ссылок (необходимость пользоваться «слабыми ссылками»).

Реализации

Существуют два вида таких указателей: с хранением счётчика внутри объекта и с хранением счётчика снаружи.

Самый простой из вариантов — хранение счётчика внутри управляемого объекта. В COM объекты с подсчётом ссылок реализуются следующим образом:

  • Объект обязан хранить внутри себя неотрицательное целое, которое означает число внешних указателей, ссылающихся на этот объект.
  • При присваивании указателю адреса нового объекта указатель вызывает у объекта метод AddRef(). Если перед этим указатель ссылался на другой объект, то сначала вызывается метод Release() прежнего объекта. При удалении указателя (выходе его из области видимости или разрушении объекта, полем которого он являлся), если этот указатель ссылается на существующий объект, указатель вызывает метод Release() объекта.
  • Реализация метода Release() каждый раз уменьшает число ссылок на единицу и сразу проверяет новое значение. Если число ссылок стало равно нулю, метод Release() вызывает удаление объекта.

Сходным образом реализован boost::intrusive_ptr.

В std::shared_ptr счётчики ссылок хранятся снаружи объекта, в специальной структуре данных. Такой умный указатель вдвое больше стандартного (в нём два поля, одно указывает на структуру-счётчик, второе — на управляемый объект). Такая конструкция позволяет:

  • Слабые указатели — указатели, которые не удерживают объекта и позволяют ему исчезать, как только пропадёт последний «сильный» указатель. Структура-счётчик удерживается, пока действует хоть один слабый указатель на пропавший объект.
  • Указатели, которые указывают на один объект, но управляют другим (например, управляют созданным через std::make_shared объектом, но указывают на его поле).
  • Умные указатели, которые ничем не управляют, только указывают (например, на глобальный объект, который живёт всё время, пока работает программа).

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

Проблема циклических ссылок

Предположим, есть два объекта и в каждом из них по владеющему указателю. Указателю в первом объекте присвоим адрес второго объекта, а указателю во втором — адрес первого объекта. Если теперь всем внешним (то есть не хранящимся внутри этих объектов) указателям на два данных объекта присвоить новые значения, то указатели внутри объектов по-прежнему будут владеть друг другом и будут оставаться в памяти. В результате возникнет ситуация, когда к объектам невозможно получить доступ, то есть утечка памяти.

Проблема циклических ссылок решается либо путём соответствующего проектирования структур данных, либо использованием сборки мусора, либо использованием двух видов ссылок: сильные (владеющие) и слабые (невладеющие, напр. std::weak_ptr).

Примеры реализаций

  • boost: boost::shared_ptr и boost::intrusive_ptr;
  • Loki: SmartPtr;
  • Поддержка COM, реализованная в большинстве компиляторов Си++ для Windows;
  • Qt: QSharedPointer, QWeakPointer и др.

Указатели единоличного владения

Часто указатели совместного владения слишком большие и «тяжёлые» для задач программиста: например, нужно создать объект одного из N типов, владеть им, время от времени обращаясь к его виртуальным функциям, а потом корректно удалить. Для этого используется «младший брат» — указатель единоличного владения.

Такие указатели при присвоении нового значения или удалении сами удаляют объект. Присвоение указателей единоличного владения возможно только с разрушением одного из указателей — таким образом, никогда не будет ситуации, что два указателя владеют одним объектом.

Их недостатком являются трудности с передачей объекта за пределы области видимости указателя.

Примеры реализаций

Указатели на чужой буфер памяти

В большинстве случаев, если есть функция, имеющая дело с массивом, пишут одно из двух:

void sort(size_t size, int* data);    // указатель + размер
void sort(std::vector<int>& data);    // конкретная структура памяти

Первое исключает автоматическую проверку диапазона. Второе ограничивает применимость std::vector’ом, и нельзя отсортировать, например, строку массива или часть другого vector’а.

Потому в развитых библиотеках для функций, которые пользуются чужими буферами памяти, используют «лёгкие» типы данных наподобие

template <class T>
struct Buf1d {
  T* data;
  size_t size;

  Buf1d(std::vector<T>& vec);
  T& operator [](size_t i);
};

Нередко используется для строк: синтаксическим разборам, обеспечению жизнедеятельности текстового редактора и прочим специфичным задачам нужны собственные структуры данных, более быстрые, чем стандартные методы работы со строками.

Примеры реализаций

  • стандартная библиотека шаблонов: std::string_view, std::span.
  • Qt: QStringView.

Примечания

  1. Элджер Д. Умные указатели как идиома // С++. Библиотека программиста. — 1999. — С. 75. — 320 с. — ISBN 0-12-049942-8.
  2. Ivor Horton, Peter Van Weert. Raw Pointers and Smart Pointers // Beginning C++17. From Novice to Professional. — 5-е. — Apress, 2018. — P. 206. — ISBN 978-1-4842-3365-8.

Ссылки

This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.