Урок №193. Умный указатель std::unique_ptr

  Юрий  | 

  |

  Обновл. 8 Окт 2021  | 

 83482

 ǀ   26 

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

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

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

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

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

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

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

  1. Олег:

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

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

    Разве item к моменту удаления не является nullptr? Ведь unique_ptr должен был украсть управление ресурсом у item и обнулить указатель.

  2. Константин:

    Вроде бы осилил:

  3. Сергей:

    operator<< рекурсивно вызывается

    измените класс и будет вам счастье 🙂

  4. Владимир:

    Здравствуйте!
    Не очень понятно, с чего это следует избегать выделение памяти для объектов, содержащих std::unique_ptr, динамически. Да, конечно, надо их удалять, причём, например, с помощью того же unique_ptr.
    Чем это хуже, чем выделение памяти для обычных объектов динамически?.. Что, теперь вообще не выделять память динамически? 🙂

  5. Алексей:

    А разве нельзя сделать так: void takeOwnership(std::unique_ptr<Item>&& item) ????

    1. Илья:

      "Rvalue ссылка ведет себя точно так же, как и lvalue ссылка, за исключением того, что она может быть связана с временным объектом, тогда как lvalue связать с временным (не константным) объектом нельзя."
      https://habr.com/ru/post/226229/

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

  6. Алексей:

    при инициализации std::unique_ptr динамически выделенным массивом необходимо писать так :

    Я же писал так :

    Что приводило к удалению лишь первого элемента.
    Я долго не мог понять почему так, и здесь примеров с массивами не видел.

    И все же этот std::unique_ptr не так уж и "умен" раз ему надо говорить что это массив.

    1. Снова я:

      Есть инструкции языка, которые программист обязан соблюдать.
      Чтобы умный указатель работал как положено с данным типом данных( массив) вы должны правильно передать этот тип данных в параметры щаблона — <Item[]> вместо <Item>

      Это должно делаться так же как ставить точку с запятой после стейтментов.

  7. Анастасия:

    Этот урок с одного захода осилить не получилось, я читала его два дня))
    И всё равно есть чувство, что я чего-то недопонимаю и надо ещё пару раз перечитать…
    В частности правило "не позволяйте нескольким классам «владеть» одним и тем же ресурсом" я бы переписала как "не позволяйте двум разным умным указателям владеть одним и тем же ресурсом".
    И ещё какой-то полный хаос опять с этими ссылками и указателями. Здесь уже было несколько уроков, после которых я возвращалась к этим темам — и вновь задавалась вопросом, в каких случаях возвращать по ссылке, по указателю и в каких по значению, а передавать по чему?
    В частности мы говорим, что в std::unique_ptr есть перегруженный оператор ->, который возвращает указатель. Дак нет же, есть ещё и метод get(), который возвращает указатель. Он делает то же самое? сомневаюсь) и так далее, и тому подобное. Мозг кипит от рассуждений вроде "как бы нам запихнуть в функцию указатель на умный указатель", а главное, зачем?..

    1. Анастасия:

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

      1. Василий:

        Метод get() и operator->() возвращают одно и то же (указатель на объект хранимый в умном указателе). Просто если будет один оператор->, передавать в функцию копию указателя на объект будет не очень удобно. По этой же причине не очень удобно вызывать методы объекта класса, которым управляет умный указатель, через метод get().

        1. Снова я:

          Ценность этого комментария сравнима с ценностью всего урока 🙂

  8. Дмитрий:

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

  9. Илья:

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

    1. Илья:

      Так лучше:

  10. Дмитрий:

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

    1. Илья:

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

  11. Pere_Strelka:

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

    1. Фото аватара Юрий:

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

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

  12. kmish:

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

  13. dshadov:

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

    1. Дмитрий:

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

  14. Евгений:

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

    1. Фото аватара Юрий:

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

      1. korvell:

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

        1. Фото аватара Юрий:

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

Добавить комментарий для Pere_Strelka Отменить ответ

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