M.8 – Проблемы с круговой зависимостью с std::shared_ptr и умный указатель std::weak_ptr
В предыдущем уроке мы увидели, как std::shared_ptr
позволяет нам иметь несколько умных указателей, совместно владеющих одним и тем же ресурсом. Однако в некоторых случаях это может стать проблемой. Рассмотрим случай, когда указатели совместного использования в двух отдельных объектах перекрестно указывают на эти объекты:
#include <iostream>
#include <memory> // для std::shared_ptr
#include <string>
class Person
{
std::string m_name;
std::shared_ptr<Person> m_partner; // изначально создается пустым
public:
Person(const std::string &name): m_name(name)
{
std::cout << m_name << " created\n";
}
~Person()
{
std::cout << m_name << " destroyed\n";
}
friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
{
if (!p1 || !p2)
return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
};
int main()
{
auto lucy { std::make_shared<Person>("Lucy") }; // создаем Person по имени "Lucy"
auto ricky { std::make_shared<Person>("Ricky") }; // создаем Person по имени "Ricky"
partnerUp(lucy, ricky); // Заставляем "Lucy" указывать на "Ricky" и наоборот
return 0;
}
В приведенном выше примере мы динамически размещаем два объекта Person
, "Lucy" и "Ricky", используя make_shared()
(чтобы гарантировать, что "Lucy" и "Ricky" будут уничтожены в конце main()
). Затем мы объединяем их. Это устанавливает std::shared_ptr
внутри "Lucy" так, чтобы он указывал на "Ricky", а std::shared_ptr
внутри "Ricky" – на "Lucy". Указатели std::shared_ptr
предназначены для совместного владения, поэтому нормально, что и указатель std::shared_ptr lucy
, и указатель std::shared_ptr m_partner
у "Ricky" указывают на "Lucy" (и наоборот).
Однако эта программа работает не так, как ожидалось:
Lucy created
Ricky created
Lucy is now partnered with Ricky
И всё. Удаления не было. Эмм... Что случилось?
После вызова partnerUp()
появляются два указателя совместного владения, указывающих на "Ricky" (ricky
и m_partner
у "Lucy"), и два указателя совместного использования, указывающих на "Lucy" (lucy
и m_partner
у "Ricky").
В конце main()
первым за пределы области видимости выходит умный указатель ricky
. Когда это происходит, ricky
проверяет, есть ли какие-либо другие указатели совместного использования, которые также являются владельцами объекта Person
"Ricky". Они есть (m_partner
у "Lucy"). Из-за этого он не освобождает "Ricky" (если бы это произошло, то m_partner
у "Lucy" превратился бы в висячий указатель). На данный момент у нас есть один указатель совместного владения на "Ricky" (m_partner
у "Lucy") и два указателя совместного владения на "Lucy" (lucy
и m_partner
у "Ricky").
Затем за пределы области видимости выходит умный указатель lucy
, и происходит то же самое. Указатель совместного владения lucy
проверяет, есть ли какие-либо другие указатели совместного владения, которые также являются владельцами объекта Person
"Lucy". Они есть (m_partner
у "Ricky"), поэтому "Lucy" не освобождается. На данный момент у нас есть один умный указатель на "Lucy" (m_partner
у "Ricky") и один умный указатель на "Ricky" (m_partner
у "Lucy").
Затем программа заканчивается – и ни один объект Person
, ни "Lucy", ни "Ricky", не удаляется! По сути, "Lucy" в конечном итоге удерживает "Ricky" от уничтожения, а "Ricky" в конечном итоге защищает от уничтожения "Lucy".
Оказывается, это может произойти в любой момент, когда указатели совместного владения образуют круговые ссылки.
Круговые ссылки
Круговые ссылки (также называемые циклическими ссылками или круговой/циклической зависимостью) – это последовательность ссылок, в которой каждый объект ссылается на следующий, а последний объект ссылается обратно на первый, вызывая ссылочную петлю. Ссылки не обязательно должны быть реальными ссылками C++ – они могут быть указателями, уникальными идентификаторами или любыми другими средствами идентификации конкретных объектов.
В контексте указателей совместного владения ссылки будут этими указателями.
Это именно то, что мы видим в приведенном выше случае: "Lucy" указывает на "Ricky", а "Ricky" указывает на "Lucy". С тремя указателями вы получите то же самое, когда A указывает на B, B указывает на C, а C указывает на A. Практический эффект от того, что указатели совместного владения образуют петлю, заключается в том, что каждый объект в конечном итоге поддерживает следующий объект живым – с последним объектом, сохраняющим в живых первый объект. Таким образом, никакие объекты в этой последовательности не могут быть удалены, потому что все они думают, что они всё еще нужны какому-то другому объекту!
Случай сокращенного цикла
Оказывается, эта проблема с циклическими ссылками может возникнуть даже с одним std::shared_ptr
– std::shared_ptr
, ссылающийся на объект, который его содержит, всё еще является циклом (просто сокращенным). Хотя маловероятно, что это когда-либо произойдет на практике, но рассмотрим пример для дополнительного понимания:
#include <iostream>
#include <memory> // для std::shared_ptr
class Resource
{
public:
std::shared_ptr<Resource> m_ptr; // изначально создается пустым
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
auto ptr1 { std::make_shared<Resource>() };
ptr1->m_ptr = ptr1; // m_ptr теперь совместно владеет объектом Resource,
// который его содержит
return 0;
}
В приведенном выше примере, когда ptr1
выходит за пределы области видимости, он не освобождает объект Resource
, потому что m_ptr
этого объекта Resource
также совместно владеет этим объектом Resource
. Теперь уже некому удалить этот объект Resource
(m_ptr
никогда не выходит за пределы области видимости, поэтому для удаления никогда не будет шанса). Таким образом, программа напечатает:
Resource acquired
и всё.
Так что же такое std::weak_ptr
?
Для решения проблемы «циклического владения», описанного выше, был разработан std::weak_ptr
. std::weak_ptr
является наблюдателем – он может наблюдать и получать доступ к тому же объекту, что и std::shared_ptr
(или другой std::weak_ptr
), но не считается владельцем. Помните, что когда указатель std::shared
выходит за пределы области видимости, он учитывает только других std::shared_ptr
, совместно владеющих объектом. std::weak_ptr
не считается!
Давайте решим нашу проблему с объектами Person
с помощью std::weak_ptr
:
#include <iostream>
#include <memory> // для std::shared_ptr и std::weak_ptr
#include <string>
class Person
{
std::string m_name;
std::weak_ptr<Person> m_partner; // обратите внимание: теперь это std::weak_ptr
public:
Person(const std::string &name): m_name(name)
{
std::cout << m_name << " created\n";
}
~Person()
{
std::cout << m_name << " destroyed\n";
}
friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
{
if (!p1 || !p2)
return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
};
int main()
{
auto lucy { std::make_shared<Person>("Lucy") };
auto ricky { std::make_shared<Person>("Ricky") };
partnerUp(lucy, ricky);
return 0;
}
Этот код ведет себя правильно:
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky destroyed
Lucy destroyed
Функционально он работает почти так же, как и проблемный пример. Однако теперь, когда ricky
выходит за пределы области видимости, он видит, что нет других std::shared_ptr
, указывающих на "Ricky" (std::weak_ptr
у "Lucy" не учитывается). Следовательно, он удалит "Ricky". То же самое и с lucy
.
Использование std::weak_ptr
Обратной стороной std::weak_ptr
является то, что std::weak_ptr
нельзя использовать напрямую (у них нет оператора ->
). Чтобы использовать std::weak_ptr
, вы должны сначала преобразовать его в std::shared_ptr
. Затем вы сможете использовать этот std::shared_ptr
. Чтобы преобразовать std::weak_ptr
в std::shared_ptr
, вы можете использовать функцию-член lock()
. Вот обновленный пример из приведенного выше:
#include <iostream>
#include <memory> // для std::shared_ptr и std::weak_ptr
#include <string>
class Person
{
std::string m_name;
std::weak_ptr<Person> m_partner; // обратите внимание: это теперь std::weak_ptr
public:
Person(const std::string &name) : m_name(name)
{
std::cout << m_name << " created\n";
}
~Person()
{
std::cout << m_name << " destroyed\n";
}
friend bool partnerUp(std::shared_ptr<Person> &p1, std::shared_ptr<Person> &p2)
{
if (!p1 || !p2)
return false;
p1->m_partner = p2;
p2->m_partner = p1;
std::cout << p1->m_name << " is now partnered with " << p2->m_name << "\n";
return true;
}
// используем lock() для преобразования weak_ptr в shared_ptr
const std::shared_ptr<Person> getPartner() const { return m_partner.lock(); }
const std::string& getName() const { return m_name; }
};
int main()
{
auto lucy { std::make_shared<Person>("Lucy") };
auto ricky { std::make_shared<Person>("Ricky") };
partnerUp(lucy, ricky);
auto partner = ricky->getPartner(); // получаем shared_ptr на партнера Ricky
std::cout << ricky->getName() << "'s partner is: " << partner->getName() << '\n';
return 0;
}
Этот код печатает:
Lucy created
Ricky created
Lucy is now partnered with Ricky
Ricky's partner is: Lucy
Ricky destroyed
Lucy destroyed
Нам не нужно беспокоиться о циклических зависимостях std::shared_ptr
с переменной partner
, поскольку это всего лишь локальная переменная внутри функции. В конечном итоге в конце функции она выйдет за пределы области видимости, и счетчик ссылок будет уменьшен на 1.
Заключение
std::shared_ptr
можно использовать, когда вам нужно несколько умных указателей, которые могут совместно владеть ресурсом. Ресурс будет освобожден, когда последний std::shared_ptr
выйдет за пределы области видимости. std::weak_ptr
можно использовать, когда вам нужен умный указатель, который может видеть и использовать общий ресурс, но не участвует во владении этим ресурсом.
Небольшой тест
Вопрос 1
Исправьте программу «сокращенного случая», чтобы Resource
удалялся правильно.
Ответ
#include <iostream> #include <memory> // для std::shared_ptr и std::weak_ptr class Resource { public: // используйте std::weak_ptr, чтобы m_ptr не поддерживал Resource живым std::weak_ptr<Resource> m_ptr; Resource() { std::cout << "Resource acquired\n"; } ~Resource() { std::cout << "Resource destroyed\n"; } }; int main() { auto ptr1 { std::make_shared<Resource>() }; ptr1->m_ptr = ptr1; // m_ptr теперь совместно использует объект Resource, // который его содержит return 0; }