Урок №193. std::unique_ptr

  Юрий  | 

    | 

  Обновл. 2 Июл 2019  | 

 4543

 ǀ   13 

Теперь, когда мы рассмотрели основы семантики перемещения, мы можем вернуться к теме умных указателей.

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

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

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

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

Стандартная библиотека в 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 (23 оценок, среднее: 4,78 из 5)
Загрузка...

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

  1. Аватар Дмитрий:

    Добрый день!
    Спасибо большое за уроки!
    В задании 2 есть вариант передачи аргумента в функцию printFraction по константной ссылке на сам объект. Какой вариант использовать предпочтительнее?

  2. Аватар Илья:

    Примерно такой ответ,я решил, что:
    а)лучше, что бы printFraction вернул указатель на дробь, не void, так мы не теряем объект,а удалить его всегда успеем;
    б)не пытайтесь передать дробь в printFraction по ссылке, т.к. объект std::unique_ptr может быть равен nullptr,и тогда полное фиаско;

  3. Аватар Дмитрий:

    с++ и так сложен наличием у себя указателей, а тут вместо того чтобы наконец то от них избавиться их еще усложняют — адский ад, ничего не понятно

    1. Аватар Илья:

      Так лучше,чем утечки иметь…

  4. Аватар Pere_Strelka:

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

    1. Юрий Юрий:

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

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

  5. Аватар kmish:

    По поводу типа auto. Не введет ли программиста в заблуждение использование auto. Мне вот, например, при виде auto ptr приходится весь код перечитывать, чтобы понять, что же там за этим авто кроется.

  6. Аватар dshadov:

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

    1. Аватар Дмитрий:

      Имеются в виду не классы а объекты этого класса. Фабрика например может создавать эти объекты.

  7. Аватар Евгений:

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

    1. Юрий Юрий:

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

      1. Аватар korvell:

        нам же лучше, меньше конкуренции 🙂
        Останутся лишь те, кто действительно хочет получить знания!

        1. Юрий Юрий:

          Конечно, порог входа не столь низок, как это может показаться. Достаточно взглянуть на просмотры первых уроков и 100-ых/200-ых 🙂

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

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