Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ООП_ Лекция №10 - Более сложные формы композиции.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
68.53 Кб
Скачать

Умные указатели std::unique_ptr

Рассмотрим следующий набросок кода, иллюстрирующий часто повторяющуюся незаметную ошибку:

// Некий полезный класс с важным методом

class MyClass

{

public:

MyClass ( int _x );

void doSomethingImportant ();

};

void f ( int _x )

{

// Выделяем объект полезного класса в динамической памяти

MyClass * pObject = new MyClass( _x );

// Если такое условие не соблюдается, выходим из функции

if ( _x < 0 )

return;

// … сделать что-нибудь нужное с объектом pObject …

pObject->doSomethingImportant();

// Уничтожаем объект

delete pObject;

}

Предположим, функция f вызвана с отрицательным аргументом, т.е. условие, приводящее к немедленному выходу из функции выполняется. До блока проверки условия в динамической памяти был выделен объект. При нормальном сценарии такой объект используется, а затем уничтожается. Однако если условие сработает и управление досрочно выйдет из функции, произойдет не что иное, как банальная утечка памяти. К сожалению, это событие произойдет незаметно, без сирены или пугающих диалогов при запуске, но память останется недоступной до завершения программы.

Другой типичный сценарий для утечки памяти - это выброс исключения (инструкция throw) между моментами выделения ресурса и его освобождения.

Стандартная библиотека содержит прекрасное решение для предотвращения проблемы подобных утечек памяти - “умные указатели” (smart pointers). При этом, обычные указатели при сравнении с умными называют “сырыми” (raw pointers). Под умным указателем понимают специальные объекты, представляющих собой объектно-ориентированные обертки над обычными указателями, автоматизирующие рутинные задачи. В частности, основной задачей умных указателей является гарантированное освобождение выделенного ресурса.

Все стандартные умные указатели подключаются к программе через заголовочный файл <memory>. Имеется несколько разновидностей стандартных умных указателей:

  • std::unique_ptr - простейший умный указатель, “защелкивающий” вверенный ему сырой указатель, гарантированно уничтожающий его в деструкторе;

  • std::shared_ptr - более сложный умный указатель, основанный на подсчете ссылок на общий разделяемый сырой указатель, освобождающий ресурс, когда уничтожается последний обладатель - применяется, в первую очередь, в случаях, когда дочерний объект не имеет однозначно определяемого объекта-родителя (ответственность за уничтожение размыта);

  • std::weak_ptr - дополнение к std::shared_ptr, предназначенное для разрыва циклических связей между двумя объектами, ссылающимися друг на друга при помощи std::shared_ptr (применяется в узко специализированных ситуациях);

  • std::auto_ptr - ранний эквивалент std::unique_ptr до принятия стандарта С++’11, уничтожает вверенный объект в деструкторе, передает ответственность за уничтожение при копировании и присвоении (в стандарте С++’11 класс std::auto_ptr признан устаревшим и более не рекомендован, однако он часто встречается на практике в более старом коде).

Кроме того, дополнительные варианты умных указателей с более сложным поведением часто предлагаются сторонними библиотеками. В контексте рассматриваемой дисциплины и, в частности, темы композиции объектов, наибольший интерес представляют умные указатели std::unique_ptr. С работой остальных классов данной категории рекомендуется ознакомиться самостоятельно.

Итак, перепишем приведенный выше пример с использованием std::unique_ptr:

#include <memory>

void f ( int _x )

{

// Выделяем объект полезного класса в динамической памяти.

// Незамедлительно помещаем его в умный указатель.

std::unique_ptr< MyClass > pObject( new MyClass( _x ) );

// Если такое условие не соблюдается, выходим из функции

if ( _x < 0 )

return;

// … сделать что-нибудь нужное с объектом pObject …

pObject->doSomethingImportant();

}

По сравнению с предыдущим вариантом здесь внесено 2 важных изменения:

  • для динамически создаваемого объекта создается обертка std::unique_ptr, и сырой указатель, возвращенный вызовом new, моментально помещается внутрь обертки;

  • в конце функции больше нет вызова оператора delete.

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

Еще один интересный момент связан с применением оператора доступа к членам класса через указатель ->. С точки зрения синтаксиса такой вызов выглядит абсолютно аналогично работе с обычным сырым указателем, однако за такой записью стоит перегруженный в std::unique_ptr оператор ->. Такой оператор, как правило, просто возвращает хранимый сырой указатель, а компилятор обеспечивает возможность вызова методов или обращения к полям.

Помимо автоматического удаления и доступа к членам сырого указателя, следует отметить такие особенности работы с std::unique_ptr:

  • объекты класса std::unique_ptr нельзя копировать ни при конструировании, ни при присвоении - это ограничение является весьма логичным, поскольку сразу две обертки не должны отвечать за уничтожение одного и того же вверенного сырого указателя;

  • объекты класса std::unique_ptr можно и нужно перемещать, что позволяет возвращать их как результат функций вместо сырых указателей

std::unique_ptr< MyClass > someFunc ( int _x )

{

return std::unique_ptr< MyClass >( new MyClass( _x ) );

}

  • запись по созданию умного указателя одновременно с сырым указателем по его аргументам можно значительно упростить, применяя вспомогательную функцию std::make_unique:

std::unique_ptr< MyClass > someFunc ( int _x )

{

// Эквивалентно полному варианту записи: // return std::unique_ptr< MyClass >( new MyClass( _x ) )

return std::make_unique< MyClass >( _x ) );

}

  • для получения ссылки на вверенный объект перегружен оператора разыменования *:

std::unique_ptr< MyClass > pObject( new MyClass( _x ) );

MyClass const & ref = * pObject;

  • как и сырые указатели, умные указатели можно использовать в условиях, т.к. у них перегружен оператор явного преобразования к типу bool:

if ( pObject )

// do something

  • чтобы отцепить вверенный объект от умного указателя без уничтожения (допустим, какая-то другая часть программы берет на себя ответственность за его уничтожение) можно воспользоваться методом release():

std::unique_ptr< MyClass > pObject( new MyClass() );

MyClass * pRawPtr = pObject.release();

// Результат: ответственность за уничтожение снимается

  • вверенный объект может быть заменен на другой либо nullptr при помощи метода reset(), при этом прежний вверенный объект будет уничтожен:

std::unique_ptr< MyClass > pObject( new MyClass() );

// результат: первый объект уничтожен, второй прикреплен

pObject->reset( new MyClass() );

// результат: второй объект уничтожен, никто не прикреплен

pObject->reset();

Фактически, работа с умными указателями не слишком отличается от работы с сырыми указателями, но при этом, программист надежно защищен от случайных утечек памяти. Использование умных указателей является центральной в популярной в литературе идиоме программирования RAII (Resource Acquisition Is Initialization - выделение ресурса есть инициализация). Эта идиома касается не только языка С++ и не только динамической памяти, а любых видов ресурсов (файлов, сетевых соединений, мьютексов и других ресурсов). Ключевая мысль состоит в том, что не имеет практического смысла пытаться продумывать и тестировать не забыл ли программист освободить ресурс в том или ином сценарии, вместо этого необходим механизм гарантированного освобождения, где программисту негде было бы ошибиться.