M.8 – Проблемы с круговой зависимостью с std::shared_ptr и умный указатель std::weak_ptr

Добавлено 22 сентября 2021 в 02:34

В предыдущем уроке мы увидели, как 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_ptrstd::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;
}

Теги

C++ / CppLearnCppstd::shared_ptrstd::weak_ptrSTL / Standard Template Library / Стандартная библиотека шаблоновДля начинающихОбучениеПрограммированиеУмные указатели

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.