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

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

Примечание.

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

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

Безопасный доступ к указателю this в сопрограмме членов класса

Дополнительные сведения о соподпрограммах и примеры кода приведены в разделе Параллельная обработка и асинхронные операции с помощью 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 приводит к созданию асинхронного объекта, который содержит неявный указатель this (через который в конечном итоге осуществляется доступ к m_value).

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

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

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

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

Но что, если экземпляр класса уничтожается до завершения асинхронной операции? Существует множество возможностей, с помощью которых экземпляр класса может выходить за пределы области до завершения асинхронного метода. Но можно имитировать это, установив для экземпляра класса значение 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;
}

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

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

Решение заключается в предоставлении асинхронной операции (сопрограммы), собственной сильной ссылки на экземпляр класса. В настоящее время запись сопрограммы фактически содержит необработанный указатель 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 и указатель this остается допустимым.

Важно!

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

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

Если сильная ссылка не подходит, то вместо этого вы можете вызвать implements::get_weak, чтобы получить слабую ссылку для this. Просто убедитесь, что можно получить сильную ссылку перед доступом к 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"";
    }
}

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

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

Сценарий

Общие сведения об обработке событий см. в статье Handle events by using delegates in C++/WinRT (Обработка событий с помощью делегатов в 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 обычным образом, без учета времени существования.

Однако бывают случаи, когда время существования объекта 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 и примерах см. в статье Author APIs with C++/WinRT (Создание 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 SDK (Windows 10, версия 1809) установить сильную или слабую ссылку там, где зарегистрирован обработчик. При этом объект получателя событий по-прежнему активен.

Для сильной ссылки просто вызовите 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), обрабатывающий событие NetworkInfNetworkInformation.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::Windows::UI::Xaml::Controls::SwapChainPanel m_swapChainPanel;
winrt::event_token m_compositionScaleChangedEventToken;

void RegisterEventHandler()
{
    m_compositionScaleChangedEventToken = m_swapChainPanel.CompositionScaleChanged([weak_this{ get_weak() }]
        (Windows::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(Windows::UI::Xaml::Controls::SwapChainPanel const& sender,
    Windows::Foundation::IInspectable const& object)
{
    // Here, we know that the "this" object is valid.
}

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

Слабые ссылки в 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>
{
    ...
}

Не имеет значения, где в пакете вариативных параметров располагается структура маркера. При запросе слабой ссылки для типа с отключенной поддержкой компилятор отобразит сообщение This is only for weak ref support (Только для поддержки слабых ссылок).

Важные API