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

  Юрий  | 

  Обновл. 9 Ноя 2020  | 

 16690

 ǀ   16 

На предыдущем уроке мы рассматривали умный указатель std::shared_ptr и то, как с его помощью сразу несколько умных указателей могут владеть одним динамически выделенным ресурсом. Однако, иногда это может быть проблематично.

Пересечение умных указателей

Например, рассмотрим случай, когда два умных указателя типа std::shared_ptr владеют двумя разными объектами и «пересекаются» между собой:

Здесь мы динамически выделяем два объекта (Anton и Ivan) класса Human и, используя std::make_shared, передаем их в два отдельно созданных умных указателя типа std::shared_ptr. Затем «связываем» их с помощью дружественной функции partnerUp().

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

Anton created
Ivan created
Anton is now partnered with Ivan

И это всё? Никаких уничтожений? Почему? Сейчас разберемся.

После вызова функции partnerUp() у нас образуется 4 умных указателя типа std::shared_ptr:

   Два умных указателя указывают на объект Ivan: ivan (из функции main()) и m_partner (из класса Human) объекта Anton.

   Два умных указателя указывают на объект Anton: anton и m_partner объекта Ivan.

В конце функции partnerUp() умный указатель ivan выходит из области видимости первым. Когда это происходит, он проверяет, есть ли другие умные указатели, которые владеют объектом Ivan класса Human. Есть — m_partner объекта Anton. Из-за этого умный указатель не уничтожает Ivan-а (если он это сделает, то m_partner объекта Anton останется висячим указателем). Таким образом у нас остается один умный указатель, владеющий Ivan-ом (m_partner объекта Anton) и два умных указателя, владеющие Anton-ом (anton и m_partner объекта Ivan).

Затем умный указатель anton выходит из области видимости, и происходит то же самое. anton проверяет, есть ли другие умные указатели, которые также владеют объектом Anton класса Human. Есть — m_partner объекта Ivan, поэтому объект Anton не уничтожается. Таким образом, остаются два умных указателя:

   m_partner объекта Ivan, который указывает на Anton-а;

   m_partner объекта Anton, который указывает на Ivan-а.

Затем программа завершает свое выполнение, и ни объект Anton, ни объект Ivan не уничтожаются! По сути, Anton не дает уничтожить Ivan-а, а Ivan не дает уничтожить Anton-а.

Это тот случай, когда умные указатели типа std::shared_ptr формируют циклическую зависимость.

Циклическая зависимость


Циклическая зависимость (или «циклические ссылки») — это серия «ссылок», где текущий объект ссылается на следующий, а последний объект ссылается на первый. Эти «ссылки» не обязательно должны быть обычными ссылками в языке C++, они могут быть указателями, уникальными ID или любыми другими средствами идентификации конкретных объектов.

В контексте std::shared_ptr этими «ссылками» являются указатели.

Это именно то, что мы видим выше: Anton указывает на Ivan-а, а Ivan указывает на Anton-а. Аналогично, A указывает на B, B указывает на C, а C указывает на A. Практическая ценность такой циклической зависимости в том, что текущий объект «оставляет в живых» (не дает уничтожить) следующий объект.

Упрощенная циклическая зависимость

Оказывается, проблема циклической ссылки может возникнуть даже с одним умным указателем типа std::shared_ptr. Такая циклическая зависимость называется упрощенной. Хотя это редко случается на практике, но все же рассмотрим и этот случай:

В примере, приведенном выше, когда ptr1 выходит из области видимости, он не уничтожает Item, поскольку член m_ptr класса Item также владеет Item-ом. Таким образом, не остается никого, кто мог бы удалить Item (m_ptr никогда не выходит из области видимости, поэтому он этого не сделает).

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

Item acquired

Всё!

Так что же такое умный указатель std::weak_ptr?


Умный указатель std::weak_ptr был разработан для решения проблемы «циклической зависимости», описанной выше. std::weak_ptr является наблюдателем — он может наблюдать и получать доступ к тому же объекту, на который указывает std::shared_ptr (или другой std::weak_ptr), но не считаться владельцем этого объекта. Помните, когда std::shared_ptr выходит из области видимости, он проверяет, есть ли другие владельцы std::shared_ptr. std::weak_ptr владельцем не считается!

Давайте перепишем первую программу этого урока, но уже с использованием std::weak_ptr:

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

Anton created
Ivan created
Anton is now partnered with Ivan
Ivan destroyed
Anton destroyed

Функционально всё работает почти идентично программе, приведенной в начале этого урока. Однако теперь, когда ivan выходит из области видимости, он видит, что нет другого std::shared_ptr, указывающего на Ivan-а (std::weak_ptr из Anton-а не считается). Поэтому он уничтожает Ivan-а. То же самое происходит и с Anton-ом.

Использование умного указателя std::weak_ptr

Недостатком умного указателя std::weak_ptr является то, что его нельзя использовать напрямую (нет оператора ->). Чтобы использовать std::weak_ptr, вы сначала должны конвертировать его в std::shared_ptr (с помощью метода lock()), а затем уже использовать std::shared_ptr. Например, перепишем вышеприведенную программу:

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

Anton created
Ivan created
Anton is now partnered with Ivan
Ivan's partner is: Anton
Ivan destroyed
Anton destroyed

Нам не нужно беспокоиться о циклической зависимости с переменной partner (типа std::shared_ptr), так как она является простой локальной переменной внутри функции main() и уничтожается при завершении выполнения функции main().

Заключение


Умный указатель std::shared_ptr используется для владения одним динамически выделенным ресурсом сразу несколькими умными указателями. Ресурс будет уничтожен, когда последний std::shared_ptr выйдет из области видимости. std::weak_ptr используется, когда нужен умный указатель, который имеет доступ к ресурсу, но не считается его владельцем.

Тест

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

Ответ


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

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

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

  1. Аватар Константин:

    Если правильно понял то так, деструктор теперь срабатывает:

  2. Аватар Алексей:

    А чем std::weak_ptr лучше обычных указателей? В данных примерах все это можно было бы решить использовав обычный указатель, ведь очищать память не надо.

    1. Аватар Алекс:

      std::weak_ptr наблюдает состояние объекта, который держат другие shared_ptr'ы.
      Т.е. можно узнать, разрушен ли объект или нет с помощью метода expired().

      Можно получить еще один shared_ptr с помощью вышеописанного метода lock(), что увеличит счетчик ссылок во всех остальных shared_ptr'ах, владеющих интересующим объектом.

      Правило-рекомендация:
      Метод lock() вызывать только в момент необходимости, его результаты не кэшировать и никуда не передавать!
      Если надо передать — то 1000 раз подумать, это звоночек!

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

    Т.е. умные указатели тоже могут привести к утечке памяти? И зачем они тогда нужны?

    Весь этот огород нужен только для того чтобы не забыть написать delete?

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

      А ещё что бы с помощью функций std::make_shared/unique избегать исключений/ошибок в конструкторах

  4. Аватар kmish:

    Прошу не удалять )) я так долго это писал.
    В статье:
    "В конце функции partnerUp(), умный указатель ivan выходит из области видимости первым…
    Затем умный указатель anton выходит из области видимости, и происходит то же самое…"
    Тут либо перевод некорректный, либо опечатка, либо просто ошибка в оригинале.
    Во-первых почему ivan выходит 1м, а anton 2м, почему не наоборот? Запросто! Можно и наоборот.
    С++ не регламентирует порядок выполнения параметров (из предыдущих уроков).
    Пол-дня размышлял над этим уроком и представленным примером…
    Код:

    Что мы делаем? Мы передаем std::shared_ptr<Human> anton и std::shared_ptr<Human> ivan по ссылке l-value.
    Это никакая ни семантика перемещения, мы просто передаем ссылку на объект(в данном случае указатель), и
    этот указатель не перетекает (перемещается) в функцию partnerUp(), это просто ссылка на объект(указатель),
    т.е. функция не становится его владельцем. В статье же утверждается именно передача владения ("В конце функции partnerUp(), умный указатель ivan выходит из области видимости") — и это ложь (или опечатка, упущение, хз).
    Это можно легко доказать добавив во 2й пример с std::weak_ptr в main проверочную строку:

    Результат:

    Anton created
    Ivan created
    Anton is now partnered with Ivan
    test
    Ivan destroyed
    Anton destroyed

    Мы видим, что объекты уничтожились после test, т.е. в конце main. А не после partnerUp(), как в статье говорится.
    Далее, если мы запишем функцию следующим образом:

    т.е. передачей по значению, то результат в Visual Studio 2017 будет:

    Anton created
    Ivan created
    Anton is now partnered with Ivan
    test
    Ivan destroyed
    Anton destroyed

    В данном случае, скорее всего, конструктор копирования игнорируется компилятором в целях оптимизации — элизия (урок 141).

    И напоследок к вопросу о циклической зависимости:

    Результат:

    Anton created
    Ivan created
    Anton is now partnered with Ivan
    test
    Anton is now partnered with Anton

    Не сложно догадаться, что происходит. Это обычная рекурсия! — ссылаемся сами на себя пока не прекратим (m_partner->m_partner->m_partner->m_partner).
    Точно также с:

    Это к тому, что эта так называемая Циклическая зависимость, является рекурсией из урока 107.

    1. Аватар kmish:

      И еще 1 момент 🙂 Чтобы реализовать передачу владения, как в статье сказано, нужно написать программу следующим образом:

      Результат:

      Anton created
      Ivan created
      Anton is now partnered with Ivan
      Anton destroyed
      Ivan destroyed
      test

      Вот теперь уничтожается после partnerUp(), причем сначала Anton!, а не Ivan (хотя могло бы быть и наоборот).

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

      ivan выходит первым, потому что переменные распологаются стеком (последним создан — первым удалён)

      1. Аватар Smith:

        Да, но в стек параметры могут помещаться в любом порядке. Зависит от компилятора, его настроек и тд

        1. Аватар Алексей:

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

    3. Аватар kolka:

      У вас все перемешалось, но в чем то вы правы.
      В тексте статьи объяснено:

      "После вызова partnerUp() у нас образуется 4 умных указателя типа std::shared_ptr:
      Два, которые указывают на объект Ivan: ivan (из функции main()) и m_partner (из класса Human) объекта Anton.

      Два, которые указывают на объект Anton: anton и m_partner объекта Ivan.".
      ,т.е. у нас 2 указателя это переданные параметры (да, передаем по ссылке, но в статье не "утверждается именно передача владения"), и 2 — это эти параметры, скопированные в m_partner.

      Вот далее непонятно написано: "В конце функции partnerUp(), умный указатель ivan выходит из области видимости первым. Когда это происходит, он проверяет, есть ли другие умные указатели, которые владеют объектом Ivan класса Human. Есть — m_partner объекта Anton. Из-за этого умный указатель не уничтожает Ivan-а "
      У нас есть умные указатели, которые владеют объектами Ivan и Anton класса Human и находятся они в мэйне, поэтому предполагаемая возможность уничтожить их в функции partnerUp() представляется сомнительной. По завершению функции partnerUp() выходят из области видимости только 2 из указанных выше указателя — это переданные параметры. При этом в самой функции идет зацикливание, при этом первый члену m_partner объекта "Anton", на который указывает умный указатель anton, присваивается указатель ivan, и наоборот. А вот в конце мейна и оставшиеся 2 указателя выходит из области видимости, но при уничтожении антона, он не уничтожается, потому что на него указывает иван, и наоборот.

      С рекурсией тоже что — то не то вы говорите.
      anton->m_partner это указатель на ivan, соответственно anton->m_partner->m_partner это указатель на anton и т.д., никакой рекурсии нет, если
      ivan->m_partner не будет указывать на антона(например nullptr), ваша последовательность anton->m_partner->m_partner->m_partner->m_partner->m_name не будет указывать ни на чье имя и вы будете разыменовывать нулевой указатель.

  5. Аватар Артем:

    Все отлично для новичков, но хотелось бы побольше нюансов и задачек. А так спасибо

  6. Аватар korvell:

    Спасибо за то, что сделал оглавление. Очень полезно 🙂

    1. Юрий Юрий:

      Пожалуйста 🙂

      1. Аватар Артёмка:

        Юра, а сколько всего уроков? 196 последний? Или будут еще?

        1. Юрий Юрий:

          Ещё перевести осталось около 20-ти уроков.

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

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