Урок №193. std::unique_ptr

  Юрий Ворон  | 

    | 

  Обновл. 31 Дек 2018  | 

 496

 ǀ   3 

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

Теперь, когда мы рассмотрели основы семантики перемещения, мы можем вернуться к теме умных указателей. Напоминаем, умный указатель — это класс, который управляет динамически выделенной памятью/ресурсом/объектом. Главная фишка умных указателей заключается в управлении и обеспечении корректного удаления динамически выделенного ресурса в соответствующее время (обычно, когда умный указатель выходит из области видимости).

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

Стандартная библиотека в C++11 имеет 4 класса умных указателей:

   std::auto_ptr (который не следует использовать — он удален в C++17);

   std::unique_ptr;

   std::shared_ptr;

   std::weak_ptr.

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

std::unique_ptr

std::unique_ptr является заменой std::auto_ptr в C++11. Вы должны использовать именно его для управления любым динамически выделенным объектом/ресурсом, но с условием, что std::unique_ptr полностью владеет переданным ему объектом, а не делится «владением» еще с другими классами. std::unique_ptr находится в заголовочном файле memory.

Рассмотрим простой пример использования std::unique_ptr:

Когда std::unique_ptr выходит из области видимости, он удаляет Item, которым владеет.

В отличие от std::auto_ptr, std::unique_ptr корректно реализовывает семантику перемещения:

Результат выполнения программы выше:

Item acquired
item1 is not null
item2 is null
Ownership transferred
item1 is null
item2 is not null
Item destroyed

Поскольку std::unique_ptr разработан с учетом семантики перемещения, то семантика копирования по умолчанию отключена. Если вы хотите передать содержимое, управляемое std::unique_ptr, то вы должны использовать семантику перемещения. В программе выше мы передаём содержимое std::unique_ptr с помощью std::move (который конвертирует item1 в r-value, которое является триггером для выполнения семантики перемещения вместо семантики копирования).

Доступ к объекту, который хранит умный указатель


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

std::unique_ptr не всегда может управлять объектом: либо потому, что объект был создан пустым (с использованием конструктора по умолчанию, либо передачей в качестве параметра nullptr), либо потому, что ресурс, которым он управлял, был перемещен в другой std::unique_ptr. Поэтому, прежде чем использовать какой-либо из этих операторов, вы должны проверить, действительно ли std::unique_ptr управляет ресурсом. К счастью, это легко сделать: std::unique_ptr имеет неявное преобразование в bool, которое возвращает true, если std::unique_ptr владеет ресурсом. Например:

Результат:

Item acquired
I am an Item!
Item destroyed

В программе выше мы используем перегруженный оператор * для доступа к объекту Item, которым владеет объект item класса std::unique_ptr, который затем мы выводим с помощью std::cout.

std::unique_ptr и динамические массивы

В отличие от std::auto_ptr, std::unique_ptr достаточно умён, чтобы знать, когда использовать «единичное» delete, а когда delete для массива, поэтому std::unique_ptr можно использовать как с «единичными» объектами, так и с динамическими массивами.

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

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

std::make_unique


В C++14 добавили новую функцию — std::make_unique(). Это шаблон функции, который создает объект типа шаблона и инициализирует его аргументами, переданными в функцию:

Результат выполнения программы выше:

7/9
0/1

Использование std::make_unique() является необязательным, но рекомендуется вместо использования std::unique_ptr. Дело в простоте. Кроме того, std::make_unique() решает проблему безопасности использования исключений, которая может возникнуть в результате неопределенного порядка обработки аргументов функции (так как С++ явно не указывает этот порядок).

Правило: Используйте std::make_unique() вместо создания std::unique_ptr и использования оператора new.

Безопасность использования исключений

Для тех, кто не понял, что эта за проблема с безопасностью использования исключений, которая прозвучала выше:

Здесь компилятору предоставляется большая гибкость при обработке вызова функции. Он может сначала выделить новый T, затем вызвать function_that_can_throw_exception(), а затем уже создать std::unique_ptr, который управляет динамически выделенным T. Если function_that_can_throw_exception() выбросит исключение, то выделенный T не будет корректно удалён, поскольку умный указатель, который должен выполнить его удаление – не успеет создаться. Это приведет к утечке памяти.

std::make_unique() лишен этой проблемы, поскольку выделение объекта T и создание std::unique_ptr происходят внутри функции std::make_unique(), где порядок обработки аргументов четко определен.

Возврат std::unique_ptr из функции


std::unique_ptr можно возвращать из функции по значению:

Здесь createItem() возвращает std::unique_ptr по значению. Если возвращаемое значение не присваивается какому-либо объекту, то оно выходит из области видимости, и Item удаляется. Если значение присваивается объекту (как показано в main()), то с помощью семантики перемещения Item перемещается из возвращаемого значения в нужный объект (в данном случае в ptr). Это делает возврат ресурсов с помощью std::unique_ptr намного безопаснее, чем возврат «необработанных» указателей!

В общем, вы не должны возвращать std::unique_ptr по адресу (вообще) или по ссылке (если у вас нет на это веских причин).

Передача std::unique_ptr в функцию

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

Результат выполнения программы выше:

Item acquired
I am an Item!
Item destroyed
Ending program

Обратите внимание, в этом случае право собственности на Item было передано в takeOwnership(), поэтому Item уничтожается в конце takeOwnership(), а не в конце main().

Однако в большинстве случаев вам не нужно будет, чтобы функция владела ресурсом. Хотя вы можете передать std::unique_ptr по ссылке (что позволит функции использовать объект без передачи ей права собственности на этот объект), вы должны делать это только тогда, когда caller может изменить передаваемый объект.

Вместо этого лучше передавать сам объект по указателю или по ссылке (в зависимости от того, является ли null допустимым аргументом). Это позволит функции остаться в стороне от управления объектом. Чтобы получить необработанный указатель на объект из std::unique_ptr, вы можете использовать метод get():

Результат выполнения программы выше:

Item acquired
I am an Item!
Ending program
Item destroyed

std::unique_ptr и классы

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

Неправильное использование std::unique_ptr

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

Например:

Хотя это синтаксически допустимо, конечным результатом будет то, что и item1, и item2 попытаются удалить Item, что приведет к неопределенному поведению/результатам.

Во-вторых, не удаляйте выделенный ресурс вручную из-под std::unique_ptr:

Если вы это сделаете, std::unique_ptr попытается удалить уже удаленный ресурс, что снова приведет к неопределенному поведению/результатам.

Обратите внимание, std::make_unique() предотвращает непреднамеренное возникновение обоих ситуаций выше.

Тест

Задание №1

Если в вашем классе есть умный указатель в качестве члена вашего класса, то почему вы должны стараться избегать динамического выделения объектов этого класса?

Ответ №1

Умные указатели в качестве членов класса удаляют свой ресурс только в том случае, если объект класса выходит из области видимости. Если вы выделите объект класса динамически и не удалите его должным образом, то объект класса никогда не выйдет из области видимости, и умный указатель не сможет очистить ресурс, который он хранит.

Задание №2

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

Ответ №2

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (8 оценок, среднее: 4,88 из 5)
Загрузка...

Комментариев: 3

  1. dshadov:

    Добрый день!
    Скажите, пожалуйста, а для каких целей надо вообще прибегать к созданию классов через оператор new? С динамическими массивами понятно, а классы выделять динамически зачем? Даже если сами классы используют внутри динамические массивы, это же не повод при их объявлении выделять память динамически.

  2. Евгений:

    Судя по количеству комментариев, до конца этих уроков дойдет далеко не каждый….

    1. Юрий Ворон Юрий Ворон:

      Вот здесь и проверяется желание — нужно ли это программирование или нет. Многие даже до 50-го урока не доходят, то к 200-ому уроку действительно самые отчаянные останутся 🙂

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *