На предыдущем уроке мы рассматривали умный указатель std::shared_ptr и то, как с его помощью сразу несколько умных указателей могут владеть одним динамически выделенным ресурсом. Однако, иногда это может быть проблематично.
Пересечение умных указателей
Например, рассмотрим случай, когда два умных указателя типа std::shared_ptr владеют двумя разными объектами и «пересекаются» между собой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> #include <memory> // для std::shared_ptr #include <string> class Human { std::string m_name; std::shared_ptr<Human> m_partner; // изначально пустой public: Human(const std::string &name): m_name(name) { std::cout << m_name << " created\n"; } ~Human() { std::cout << m_name << " destroyed\n"; } friend bool partnerUp(std::shared_ptr<Human> &h1, std::shared_ptr<Human> &h2) { if (!h1 || !h2) return false; h1->m_partner = h2; h2->m_partner = h1; std::cout << h1->m_name << " is now partnered with " << h2->m_name << "\n"; return true; } }; int main() { auto anton = std::make_shared<Human>("Anton"); // создание умного указателя с объектом Anton класса Human auto ivan = std::make_shared<Human>("Ivan"); // создание умного указателя с объектом Ivan класса Human partnerUp(anton, ivan); // Anton указывает на Ivan-а, а Ivan указывает на Anton-а return 0; } |
Здесь мы динамически выделяем два объекта (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. Такая циклическая зависимость называется упрощенной. Хотя это редко случается на практике, но все же рассмотрим и этот случай:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <memory> // для std::shared_ptr class Item { public: std::shared_ptr<Item> m_ptr; // изначально пустой Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { auto ptr1 = std::make_shared<Item>(); ptr1->m_ptr = ptr1; // m_ptr теперь является владельцем Item-а, членом которого он является сам return 0; } |
В примере, приведенном выше, когда 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> #include <memory> // для std::shared_ptr и std::weak_ptr #include <string> class Human { std::string m_name; std::weak_ptr<Human> m_partner; // обратите внимание, здесь std::weak_ptr public: Human(const std::string &name): m_name(name) { std::cout << m_name << " created\n"; } ~Human() { std::cout << m_name << " destroyed\n"; } friend bool partnerUp(std::shared_ptr<Human> &h1, std::shared_ptr<Human> &h2) { if (!h1 || !h2) return false; h1->m_partner = h2; h2->m_partner = h1; std::cout << h1->m_name << " is now partnered with " << h2->m_name << "\n"; return true; } }; int main() { auto anton = std::make_shared<Human>("Anton"); auto ivan = std::make_shared<Human>("Ivan"); partnerUp(anton, ivan); return 0; } |
Результат выполнения программы:
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. Например, перепишем вышеприведенную программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include <iostream> #include <memory> // для std::shared_ptr и std::weak_ptr #include <string> class Human { std::string m_name; std::weak_ptr<Human> m_partner; // обратите внимание, здесь std::weak_ptr public: Human(const std::string &name) : m_name(name) { std::cout << m_name << " created\n"; } ~Human() { std::cout << m_name << " destroyed\n"; } friend bool partnerUp(std::shared_ptr<Human> &h1, std::shared_ptr<Human> &h2) { if (!h1 || !h2) return false; h1->m_partner = h2; h2->m_partner = h1; std::cout << h1->m_name << " is now partnered with " << h2->m_name << "\n"; return true; } const std::shared_ptr<Human> getPartner() const { return m_partner.lock(); } // используем метод lock() для конвертации std::weak_ptr в std::shared_ptr const std::string& getName() const { return m_name; } }; int main() { auto anton = std::make_shared<Human>("Anton"); auto ivan = std::make_shared<Human>("Ivan"); partnerUp(anton, ivan); auto partner = ivan->getPartner(); // передаем partner-у содержимое умного указателя, которым владеет ivan std::cout << ivan->getName() << "'s partner is: " << partner->getName() << '\n'; return 0; } |
Результат выполнения программы:
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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <memory> // для std::shared_ptr и std::weak_ptr class Item { public: std::weak_ptr<Item> m_ptr; // используем std::weak_ptr, чтобы m_ptr не поддерживал Item-а Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { auto ptr1 = std::make_shared<Item>(); ptr1->m_ptr = ptr1; // m_ptr теперь является владельцем Item-а, членом которого он является сам return 0; } |
Если правильно понял то так, деструктор теперь срабатывает:
А чем std::weak_ptr лучше обычных указателей? В данных примерах все это можно было бы решить использовав обычный указатель, ведь очищать память не надо.
std::weak_ptr наблюдает состояние объекта, который держат другие shared_ptr'ы.
Т.е. можно узнать, разрушен ли объект или нет с помощью метода expired().
Можно получить еще один shared_ptr с помощью вышеописанного метода lock(), что увеличит счетчик ссылок во всех остальных shared_ptr'ах, владеющих интересующим объектом.
Правило-рекомендация:
Метод lock() вызывать только в момент необходимости, его результаты не кэшировать и никуда не передавать!
Если надо передать — то 1000 раз подумать, это звоночек!
Т.е. умные указатели тоже могут привести к утечке памяти? И зачем они тогда нужны?
Весь этот огород нужен только для того чтобы не забыть написать delete?
А ещё что бы с помощью функций std::make_shared/unique избегать исключений/ошибок в конструкторах
Прошу не удалять )) я так долго это писал.
В статье:
"В конце функции 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 момент 🙂 Чтобы реализовать передачу владения, как в статье сказано, нужно написать программу следующим образом:
Результат:
Anton created
Ivan created
Anton is now partnered with Ivan
Anton destroyed
Ivan destroyed
test
Вот теперь уничтожается после partnerUp(), причем сначала Anton!, а не Ivan (хотя могло бы быть и наоборот).
ivan выходит первым, потому что переменные распологаются стеком (последним создан — первым удалён)
Да, но в стек параметры могут помещаться в любом порядке. Зависит от компилятора, его настроек и тд
C++ гарантирует, что у локальных переменных в одной области видимости деструкторы будут вызваны в порядке, обратном конструированию объектов.
У вас все перемешалось, но в чем то вы правы.
В тексте статьи объяснено:
"После вызова 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 не будет указывать ни на чье имя и вы будете разыменовывать нулевой указатель.
Внутри функции partnerUp() выходят из области видимости ссылки на умные указатели(у них свой отдельный адрес на который они указывают), а сами же указатели остаются в порядке так как их область видимости находится в функции main. В итоге после функции partnerUp() вссе четыре указателя остаются не уничтоженными, а два.
Добавил в первый пример из урока геттер для поля m_partner, вывод адреса на который указывает ссылка, а также вывод адресов умных указателей и полей m_partner.
Anton created
Ivan created
Addres of reference h1: 003FFA14
Addres of reference h2: 003FFA04
Anton is now partnered with Ivan
Addres of anton: 00A4551C
Addres of ivan: 00A4501C
Addres of anton->m_partner: 00A4501C
Addres of ivan->m_partner: 00A4551C
Здесь видно что ссылка указывает на указатель, а не на объект. Соответственно в конце функции partnerUp уничтожится только ссылка, а не ссылка вместе с указателем Также тот факт, что после этой функции все еще можно обратится, что к умному указателю, что к полю класса говорит о том что все четыре указателя все еще существуют.
Все отлично для новичков, но хотелось бы побольше нюансов и задачек. А так спасибо
Спасибо за то, что сделал оглавление. Очень полезно 🙂
Пожалуйста 🙂
Юра, а сколько всего уроков? 196 последний? Или будут еще?
Ещё перевести осталось около 20-ти уроков.