Сильные и слабые ссылки в C++/WinRT

Это важно

Разрабатываете с помощью Windows App SDK? В коде этой статьи используются пространства имен UWP (Windows.UI.Xaml). Если ваш проект ориентирован на WinUI 3 (Windows App SDK), замените Microsoft.UI.Xaml (и связанные пространства имен Microsoft.UI.*) повсюду. Дополнительные сведения см. в руководстве по сопоставлению API UWP с Windows App SDK для полного сопоставления и миграции пользовательского интерфейса.

среда выполнения Windows — это система с подсчётом ссылок; и в такой системе вам важно понимать значение сильных и слабых ссылок и различие между ними (а также ссылок, которые не относятся ни к тем, ни к другим, таких как неявный указатель this). Как вы увидите в этом разделе, умение правильно управлять этими ссылками может стать решающим фактором, определяющим разницу между надёжной системой, которая работает стабильно, и системой, которая непредсказуемо выходит из строя. Предоставляя вспомогательные функции, тесно интегрированные с языковой проекцией, C++/WinRT облегчает вам задачу создания более сложных систем простым и правильным способом.

Note

За немногими исключениями поддержка слабых ссылок включена по умолчанию для типов среда выполнения Windows, которые вы используете или создаете в C++/WinRT. Windows. Пользовательский интерфейс. Композиция и Windows. Devices.Input.PenDevice — это примеры исключений, то есть пространства имен, в которых слабая поддержка ссылок не включена для этих типов. Кроме того, см. статью "Если делегат автоматического отзыва не удается зарегистрировать".

Если вы создаете типы, см. раздел "Слабые ссылки" в разделе C++/WinRT в этом разделе.

Безопасный доступ к этому указателю в соутине класса-члена

Дополнительные сведения о корутинах и примерах кода см. в разделе "Параллелизм" и асинхронные операции с C++/WinRT.

В приведенном ниже примере кода показан типичный пример корутины, являющейся функцией-членом класса. Этот пример можно скопировать в указанные файлы в новом проекте консольного приложения Windows (C++/WinRT).

// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>

// main.cpp : Defines the entry point for the console application.
#include "pch.h"

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

struct MyClass : winrt::implements<MyClass, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    IAsyncOperation<winrt::hstring> RetrieveValueAsync()
    {
        co_await 5s;
        co_return m_value;
    }
};

int main()
{
    winrt::init_apartment();

    auto myclass_instance{ winrt::make_self<MyClass>() };
    auto async{ myclass_instance->RetrieveValueAsync() };

    winrt::hstring result{ async.get() };
    std::wcout << result.c_str() << std::endl;
}

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

Помните, что в корутине выполнение является синхронным вплоть до первой точки приостановки, в которой управление возвращается вызывающему коду. В RetrieveValueAsync первый co_await — это первая точка приостановки. К тому времени, когда корутина возобновит выполнение (примерно через пять секунд в данном случае), с неявным указателем this, через который мы обращаемся к m_value, могло произойти что угодно.

Ниже приведена полная последовательность событий.

  1. В основном создается экземпляр MyClass (myclass_instance).
  2. Объект async создаётся и указывает (через свой this) на myclass_instance.
  3. Функция winrt::Windows::Foundation::IAsyncAction::get попадает в первую точку приостановки, блокируется в течение нескольких секунд, а затем возвращает результат GetValueAsync.
  4. RetrieveValueAsync возвращает значение this->m_value.

Шаг 4 является безопасным только до тех пор, пока это остается допустимым.

Но что делать, если экземпляр класса уничтожен до завершения асинхронной операции? Есть множество способов, которыми экземпляр класса может выйти из области видимости до того, как асинхронный метод завершится. Но мы можем имитировать его, установив для экземпляра класса значение nullptr.

int main()
{
    winrt::init_apartment();

    auto myclass_instance{ winrt::make_self<MyClass>() };
    auto async{ myclass_instance->RetrieveValueAsync() };
    myclass_instance = nullptr; // Simulate the class instance going out of scope.

    winrt::hstring result{ async.get() }; // Behavior is now undefined; crashing is likely.
    std::wcout << result.c_str() << std::endl;
}

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

При этом изменении кода возникает проблема на шаге 4, так как экземпляр класса был уничтожен, и это больше не является допустимым. Как только асинхронный объект попытается обратиться к переменной в экземпляре класса, он аварийно завершится (или сделает нечто совершенно неопределенное).

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

Чтобы сохранить экземпляр класса в живых, измените реализацию RetrieveValueAsync на то, что показано ниже.

IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
    auto strong_this{ get_strong() }; // Keep *this* alive.
    co_await 5s;
    co_return m_value;
}

Класс C++/WinRT напрямую или косвенно наследуется от шаблона winrt::implements. Поэтому объект C++/WinRT может вызывать свою защищённую функцию-член implements::get_strong, чтобы получить сильную ссылку на указатель this. Обратите внимание, что на самом деле нет необходимости использовать strong_this переменную в приведенном выше примере кода. Просто вызывая get_strong увеличивает число ссылок объекта C++/WinRT и сохраняет его неявный этот указатель допустимым.

Это важно

Поскольку get_strong является функцией-членом шаблона структуры winrt::implements, ее можно вызывать только в классе, который прямо или косвенно наследуется от winrt::implements, например в классе C++/WinRT. Дополнительные сведения о производных от winrt::implements и примерах см. в статье "Создание API с помощью C++/WinRT".

Это устраняет проблему, которую мы ранее имели, когда мы добрались до шага 4. Даже если все остальные ссылки на экземпляр класса исчезнут, корутина позаботилась о том, чтобы её зависимости оставались стабильными.

Если сильная ссылка не подходит, можно вместо этого вызвать implements::get_weak, чтобы получить слабую ссылку на объект this. Просто убедитесь, что вы можете получить надежную ссылку перед доступом к этому. Опять же, get_weak — это функция-член шаблона структуры winrt::implements.

IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
    auto weak_this{ get_weak() }; // Maybe keep *this* alive.

    co_await 5s;

    if (auto strong_this{ weak_this.get() })
    {
        co_return m_value;
    }
    else
    {
        co_return L"";
    }
}

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

Безопасный доступ к этому указателю с помощью делегата обработки событий

Сценарий

Общие сведения об обработке событий см. в разделе "Обработка событий с помощью делегатов в C++/WinRT".

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

Приведенный ниже код определяет простой класс EventSource , который вызывает универсальное событие, которое обрабатывается любыми делегатами, добавленными в него. В этом примере событие использует тип делегата Windows::Foundation::EventHandler, но описанные здесь проблемы и способы их решения применимы к любым типам делегатов.

Затем класс EventRecipient предоставляет обработчик события EventSource::Event в виде лямбда-функции.

// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>

// main.cpp : Defines the entry point for the console application.
#include "pch.h"

using namespace winrt;
using namespace Windows::Foundation;

struct EventSource
{
    winrt::event<EventHandler<int>> m_event;

    void Event(EventHandler<int> const& handler)
    {
        m_event.add(handler);
    }

    void RaiseEvent()
    {
        m_event(nullptr, 0);
    }
};

struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    void Register(EventSource& event_source)
    {
        event_source.Event([&](auto&& ...)
        {
            std::wcout << m_value.c_str() << std::endl;
        });
    }
};

int main()
{
    winrt::init_apartment();

    EventSource event_source;
    auto event_recipient{ winrt::make_self<EventRecipient>() };
    event_recipient->Register(event_source);
    event_source.RaiseEvent();
}

Суть шаблона в том, что получатель события имеет обработчик события в виде лямбда-функции, зависящий от его указателя this. Если получатель события существует дольше, чем источник события, то он существует дольше и этих зависимостей. И в таких случаях, которые являются общими, шаблон работает хорошо. Некоторые из этих случаев очевидны, например, когда страница пользовательского интерфейса обрабатывает событие, вызываемое элементом управления, который находится на странице. Страница существует дольше, чем кнопка, — поэтому обработчик тоже существует дольше, чем кнопка. Это справедливо всякий раз, когда объект-получатель владеет источником (например, как полем данных), а также всякий раз, когда объект-получатель и источник являются одноуровневыми объектами и напрямую принадлежат какому-либо другому объекту.

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

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

  • Когда источник событий генерирует свои события синхронно, вы можете отписать обработчик и быть уверенным, что больше не будете получать события. Но для асинхронных событий даже после отзыва (и особенно при отмене в деструкторе) событие в полете может достичь объекта после начала деструкции. Если предусмотреть возможность отписаться перед уничтожением, это может смягчить проблему, но читайте дальше, чтобы узнать о более надёжном решении.
  • Если вы пишете сопрограмму для реализации асинхронного метода, то это возможно.
  • В редких случаях с некоторыми объектами платформы интерфейса XAML (например, SwapChainPanel) это возможно, если объект-получатель уничтожается без отмены подписки на источник событий.

Проблема

Следующая версия функции main моделирует ситуацию, когда получатель события уничтожается (например, выходит из области видимости), в то время как источник события по-прежнему генерирует события.

int main()
{
    winrt::init_apartment();

    EventSource event_source;
    auto event_recipient{ winrt::make_self<EventRecipient>() };
    event_recipient->Register(event_source);
    event_recipient = nullptr; // Simulate the event recipient going out of scope.
    event_source.RaiseEvent(); // Behavior is now undefined within the lambda event handler; crashing is likely.
}

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

Это важно

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

Или — если это имеет смысл в вашем сценарии и если это вообще возможно с учётом особенностей многопоточности — другой вариант состоит в том, чтобы отменить регистрацию обработчика после того, как получатель завершит обработку события, или в деструкторе получателя. См. раздел "Отзыв зарегистрированного делегата".

Вот как мы регистрируем этот обработчик.

event_source.Event([&](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

Лямбда автоматически захватывает любые локальные переменные по ссылке. Таким образом, в этом примере мы могли бы эквивалентно написать это.

event_source.Event([this](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

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

Решение

Решение состоит в том, чтобы захватить сильную ссылку (или, как мы увидим, слабую ссылку, если это более уместно). Надежная ссылка увеличивает число ссылок и сохраняет текущий объект в живых. Вы просто объявляете переменную захвата (с именем strong_this в этом примере) и инициализируете её вызовом implements::get_strong, который возвращает строгую ссылку на наш указатель this.

Это важно

Поскольку get_strong является функцией-членом шаблона структуры winrt::implements, ее можно вызывать только в классе, который прямо или косвенно наследуется от winrt::implements, например в классе C++/WinRT. Дополнительные сведения о производных от winrt::implements и примерах см. в статье "Создание API с помощью C++/WinRT".

event_source.Event([this, strong_this { get_strong()}](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

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

event_source.Event([strong_this { get_strong()}](auto&& ...)
{
    std::wcout << strong_this->m_value.c_str() << std::endl;
});

Если сильная ссылка не подходит, можно вместо этого вызвать implements::get_weak, чтобы получить слабую ссылку на объект this. Слабая ссылка не сохраняет текущий объект в живых. Таким образом, просто убедитесь, что вы по-прежнему можете получить сильную ссылку из слабой ссылки перед доступом к членам.

event_source.Event([weak_this{ get_weak() }](auto&& ...)
{
    if (auto strong_this{ weak_this.get() })
    {
        std::wcout << strong_this->m_value.c_str() << std::endl;
    }
});

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

Если вы используете функцию-член в качестве делегата

Эти принципы применимы не только к лямбда-функциям, но и к использованию функции-члена в качестве делегата. Синтаксис отличается, поэтому рассмотрим некоторый код. Во-первых, вот потенциально небезопасный обработчик события, являющийся функцией-членом и использующий сырой указатель this.

struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    void Register(EventSource& event_source)
    {
        event_source.Event({ this, &EventRecipient::OnEvent });
    }

    void OnEvent(IInspectable const& /* sender */, int /* args */)
    {
        std::wcout << m_value.c_str() << std::endl;
    }
};

Это стандартный, общепринятый способ обращения к объекту и его функции-члену. Чтобы сделать это безопасным, вы можете (начиная с версии 10.0.17763.0 (Windows 10 версии 1809) пакета SDK Windows— установить сильную или слабую ссылку в точке регистрации обработчика. На этом этапе объект получателя события, как известно, по-прежнему жив.

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

event_source.Event({ get_strong(), &EventRecipient::OnEvent });

Захват сильной ссылки означает, что объект сможет быть уничтожен только после отмены регистрации обработчика и завершения всех выполняющихся обратных вызовов. Однако эта гарантия действительна только во время возникновения события. Если обработчик события является асинхронным, то перед первой точкой приостановки вам потребуется предоставить вашей корутине строгую ссылку на экземпляр класса (подробности и код см. выше в этом разделе, в разделе Безопасный доступ к указателю this в корутине-члене класса). Но это создает циклическую ссылку между источником событий и объектом, поэтому необходимо явно разорвать это, отменив событие.

Чтобы получить слабую ссылку, вызовите get_weak. C++/WinRT гарантирует, что полученный делегат содержит слабую ссылку. В последний момент и неявно делегат пытается преобразовать слабую ссылку в сильную и вызывает функцию-член только в том случае, если это удаётся.

event_source.Event({ get_weak(), &EventRecipient::OnEvent });

Если делегат действительно вызывает вашу функцию-член, то C++/WinRT будет поддерживать существование вашего объекта до тех пор, пока ваш обработчик не завершит работу. Однако если обработчик является асинхронным, он возвращается в точках приостановки, и поэтому вам придется дать корутине сильную ссылку на экземпляр класса до первой точки приостановки. И снова, дополнительные сведения см. в разделе «Безопасный доступ к указателю this в корутине-члене класса» ранее в этой теме.

Если функция-член не принадлежит типу среда выполнения Windows

Если метод get_strong недоступен для вас (тип не является типом среда выполнения Windows), можно использовать метод, показанный в примере кода ниже. Здесь показан обычный класс C++ (с именем ConsoleNetworkWatcher) для обработки события NetworkInformation.NetworkStatusChanged .

#include <winrt/Windows.Networking.Connectivity.h>
using namespace winrt;
using namespace Windows::Networking::Connectivity;

class ConsoleNetworkWatcher
{
    /* any constructor, and instance methods, here*/

    static void Initialize(std::shared_ptr<ConsoleNetworkWatcher> instance)
    {
        auto weakPointer{ std::weak_ptr{ instance } };

        instance->m_statusChangedRevoker =
            NetworkInformation::NetworkStatusChanged(winrt::auto_revoke,
                [weakPointer](winrt::Windows::Foundation::IInspectable const& sender)
                {
                    auto sharedPointer{ weakPointer.lock() };

                    if (sharedPointer)
                    {
                        sharedPointer->NetworkStatusChanged(sender);
                    }
                });
    }

    void NetworkStatusChanged(winrt::Windows::Foundation::IInspectable const& sender){/* handle event here */};

private:
    NetworkInformation::NetworkStatusChanged_revoker m_statusChangedRevoker;
};

Пример слабой ссылки с помощью SwapChainPanel::CompositionScaleChanged

В этом примере кода мы используем событие SwapChainPanel::CompositionScaleChanged в качестве еще одной иллюстрации слабых ссылок. Код регистрирует обработчик события с помощью лямбда-выражения, которое захватывает слабую ссылку на получателя.

winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel m_swapChainPanel;
winrt::event_token m_compositionScaleChangedEventToken;

void RegisterEventHandler()
{
    m_compositionScaleChangedEventToken = m_swapChainPanel.CompositionScaleChanged([weak_this{ get_weak() }]
        (Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
        Windows::Foundation::IInspectable const& object)
    {
        if (auto strong_this{ weak_this.get() })
        {
            strong_this->OnCompositionScaleChanged(sender, object);
        }
    });
}

void OnCompositionScaleChanged(Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
    Windows::Foundation::IInspectable const& object)
{
    // Here, we know that the "this" object is valid.
}

В предложении захвата лямбды создаётся временная переменная, которая представляет собой слабую ссылку на this. В теле лямбда-выражения, если удаётся получить сильную ссылку на this, вызывается функция OnCompositionScaleChanged. Таким образом, внутри OnCompositionScaleChangedэто можно безопасно использовать.

Слабые ссылки в C++/WinRT

Выше мы увидели, что используются слабые ссылки. В целом они хорошо подходят для разрыва циклических ссылок. Например, для собственной реализации платформы пользовательского интерфейса на основе XAML (из-за исторической структуры платформы) требуется слабый механизм ссылок в C++/WinRT для обработки циклических ссылок. Однако вне XAML вам, скорее всего, не придётся использовать слабые ссылки (не то чтобы в них было что-то по своей сути специфичное именно для XAML). Скорее, вы должны, чаще всего, иметь возможность создавать собственные API C++/WinRT таким образом, чтобы избежать необходимости циклических ссылок и слабых ссылок.

Для любого типа, который вы объявляете, C++/WinRT не сразу может определить, нужны ли слабые ссылки и если да, то когда. Таким образом, C++/WinRT автоматически обеспечивает поддержку слабых ссылок в шаблоне структуры winrt::implements, от которого ваши типы C++/WinRT прямо или косвенно наследуются. Это модель «платишь только при использовании»: она ничего вам не стоит, если только у вашего объекта действительно не запрашивают IWeakReferenceSource. И вы можете явно отказаться от этой поддержки.

Примеры кода

Шаблон структуры winrt::weak_ref является одним из вариантов получения слабой ссылки на экземпляр класса.

Class c;
winrt::weak_ref<Class> weak{ c };

Или можно использовать вспомогательную функцию winrt::make_weak.

Class c;
auto weak = winrt::make_weak(c);

Создание слабой ссылки не влияет на количество ссылок на сам объект; Это просто приводит к выделению блока управления. Этот блок управления заботится о реализации слабой семантики ссылок. Затем вы можете попытаться повысить слабую ссылку на сильную ссылку и, если это успешно, используйте ее.

if (Class strong = weak.get())
{
    // use strong, for example strong.DoWork();
}

При условии, что ещё существует другая сильная ссылка, вызов weak_ref::get увеличивает счётчик ссылок и возвращает сильную ссылку вызывающей стороне.

Отключение поддержки слабых ссылок

Слабая поддержка ссылок является автоматической. Но вы можете явно отказаться от этой поддержки, передав структуру-маркер winrt::no_weak_ref в качестве аргумента шаблона для вашего базового класса.

Если вы наследуете непосредственно от winrt::implements.

struct MyImplementation: implements<MyImplementation, IStringable, no_weak_ref>
{
    ...
}

Если вы создаете класс среды выполнения.

struct MyRuntimeClass: MyRuntimeClassT<MyRuntimeClass, no_weak_ref>
{
    ...
}

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

Важные API