C++17 STL Стандартная библиотека шаблонов — страница 89 из 119

f1's use counter at 1

f1's use counter at 2

DTOR bar

Back to outer scope

1

first f()

call

f: use counter at 2

second f() call

f: use counter at 1

DTOR foo

end of main()


Как это работает

При создании и удалении объектов

shared_ptr
работает аналогично
unique_ptr
. Создание общих указателей выглядит так же, как и создание уникальных указателей (однако существует функция
make_shared
, которая создает общие объекты в дополнение к функции
make_unique
для уникальных указателей
unique_ptr
).

Основное отличие от

unique_ptr
заключается в том, что можно копировать экземпляры
shared_ptr
, поскольку общие указатели поддерживают так называемый блок управления вместе с объектом, которым они управляют. Блок управления содержит указатель на объект и счетчик ссылок или счетчик использования. Если на объект указывают
N
экземпляров
shared_ptr
, то счетчик использования имеет значение
N
. Когда экземпляр типа
shared_ptr
разрушается, его деструктор уменьшает значение этого внутреннего счетчика использования. Последний общий указатель на такой объект при разрушении снизит значение счетчика использования до
0
. В данном случае будет вызван оператор
delete
для объекта! Таким образом, мы не можем допустить утечку памяти, поскольку счетчик ссылок объекта отслеживается автоматически.

Чтобы проиллюстрировать эту идею, взглянем на рис. 8.2.

В шаге 1 у нас имеются два экземпляра типа

shared_ptr
, управляющих объектом типа
Foo
. Значение счетчика использования установлено на
2
. Далее
shared_ptr2
уничтожается, это снижает значение счетчика использования до
1
. Экземпляр
Foo
пока не уничтожается, поскольку все еще существует второй общий указатель. В шаге
3
последний общий указатель также уничтожается. Это приводит к тому, что значение счетчика использования становится равным
0
. Шаг 4 выполняется сразу после шага 3. Блок управления и экземпляр типа
Foo
уничтожаются, и занятая ими память возвращается в кучу.

С помощью

shared_ptr
и
unique_ptr
можно автоматически справиться с большинством объектов, память для которых выделяется динамически, не беспокоясь об утечках памяти. Следует, однако, рассмотреть один важный подводный камень. Представьте, что у нас имеются два объекта в куче, которые содержат общие указатели друг на друга, и какой-то другой общий указатель, указывающий на один из них откуда-то еще. Если этот внешний указатель выйдет за пределы области видимости, то счетчики использования обоих объектов все еще будут иметь ненулевые значения, поскольку ссылаются друг на друга. Это приводит к утечке памяти. В подобной ситуации общие указатели применять нельзя, поскольку цепочки таких циклических ссылок не дают снизить значение счетчика использования до
0
.


Дополнительная информация

Рассмотрим следующий код. Допустим, вам сказали, что он провоцирует потенциальную утечку памяти.


void function(shared_ptr, shared_ptr, int);

// "function" определена где-то еще


// ...далее по коду:

function(new A{}, new B{}, other_function());


Кто-то спросит: «Где же утечка?» — ведь объекты

A
и
B
, для которых только что выделена память, мгновенно передаются в экземпляры типа
shared_ptr
, и это позволяет обезопасить нас от утечек.

Да, это так, утечки памяти нам не грозят до тех пор, пока указатели хранятся в экземплярах типа

shared_ptr
. Эту проблему решить довольно сложно.

Когда мы вызываем функцию

f(x(),y(),z())
, компилятор должен собрать код, который сначала вызывает функции
x()
,
y()
и
z()
, чтобы он мог перенаправить результат их работы в функцию
f
. Нас не устраивает то, что компилятор может выполнить вызовы функций
x
,
y
и
z
в любом порядке.

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

new A{}
, затем —
other_function()
, а затем —
B{}
, прежде чем результаты работы этих функций будут переданы далее? Если функция
other_function()
сгенерирует исключение, то мы получим утечку памяти, поскольку у нас в куче все еще будет неуправляемый объект
A
, поскольку мы не имели возможности передать его под управление
shared_ptr
. Независимо от того, как мы обработаем исключение, дескриптор объекта пропадет, и мы не сможем удалить его!

Существует два простых способа обойти эту проблему:


// 1.)

function(make_shared(), make_shared(), other_function());


// 2.)

shared_ptr ap {new A{}};

shared_ptr bp {new B{}};

function(ap, bp, other_function());


Таким образом, объекты уже попадут под управление

shared_ptr
независимо от того, где позднее будет сгенерировано исключение.

Работаем со слабыми указателями на разделяемые объекты

 Из примера, посвященного

shared_ptr
, мы узнали, какими полезными и простыми в использовании являются общие указатели. Вместе с
unique_pt
r они предоставляют бесценную возможность по улучшению нашего кода, нуждающегося в управлении объектами, память для которых выделяется динамически.

Копируя

shared_ptr
, мы увеличиваем его внутренний счетчик ссылок. До тех пор, пока мы храним нашу копию общего указателя, объект, на который он указывает, не будет удален. Но если нужно что-то вроде слабого указателя, который позволит получать объект до тех пор, пока тот существует, но не мешает его удалению? Как мы определим, существует ли еще объект?

В таких ситуациях нам поможет

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


Как это делается

В этом примере мы реализуем программу, которая поддерживает объекты, используя экземпляры типа

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


1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространства имен

std
по умолчанию:


#include 

#include 

#include 


using namespace std;


2. Затем реализуем класс, чей деструктор выводит на экран сообщение. Таким образом, нам будет проще проверить факт уничтожения объекта.


struct Foo {

  int value;

  Foo(int i) : value{i} {}

  ~Foo() { cout << "DTOR Foo " << value << '\n'; }

};


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

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