C++/WinRT 中的强引用和弱引用

Important

使用Windows 应用 SDK进行构建? 本文的代码使用 UWP (Windows.UI.Xaml) 命名空间。 如果项目面向 WinUI 3(Windows 应用 SDK),请在整个过程中替换Microsoft.UI.Xaml(和相关Microsoft.UI.*命名空间)。 有关更多详细信息,请参阅将 UWP API 映射到Windows 应用 SDK,了解完整的映射和 UI 迁移指南

Windows 运行时是引用计数的系统;在此类系统中,必须了解强引用和弱引用的重要性(以及两者都没有的引用,如隐式指针)。 如本主题中所示,了解如何正确管理这些引用可能意味着运行平稳的可靠系统与崩溃不可预知的系统之间的差异。 通过提供在语言投影中获得深度支持的辅助函数,C++/WinRT 可为你分担一部分工作,从而让你能够简单且正确地构建更复杂的系统。

注释

除少数例外情况外,你在 C++/WinRT 中使用或编写的 Windows 运行时 类型默认都启用了弱引用支持。 Windows.UI.CompositionWindows.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 是第一个挂起点。 等协程恢复时(本例中大约是五秒后),我们借以访问 m_value 的隐式 this 指针可能已经发生了任何变化。

下面是事件的完整序列。

  1. main 中,创建了 MyClass 实例(myclass_instance)。
  2. 创建了 async 对象,它通过其 this 指向 myclass_instance
  3. winrt::Windows::Foundation::IAsyncAction::get 函数到达其第一个挂起点后,会阻塞几秒钟,然后返回 RetrieveValueAsync 的结果。
  4. RetrieveValueAsync 返回值 this->m_value

只要 步骤 4 保持有效,步骤 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;
}

在销毁该类实例之后,看起来我们就不会再直接引用该实例了。 但当然,这个异步对象有一个指向它的 this 指针,并试图通过该指针复制存储在该类实例中的值。 协程是成员函数,并且会认为自己可以放心地使用其 this 指针。

通过对代码的此更改,我们在步骤 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 对象的引用计数,并使其隐式 指针有效。

Important

由于 get_strongwinrt::implements 结构模板的成员函数,因此只能从直接或间接派生自 winrt::implements 的类(如 C++/WinRT 类)调用它。 有关从 winrt::implements 派生的详细信息和示例,请参阅 使用 C++/WinRT 编写 API

这解决了我们之前在到达步骤 4 时出现的问题。 即使指向该类实例的所有其他引用都消失了,协程也已预先确保其依赖关系保持稳定。

如果强引用不合适,则可以改为调用 implements::get_weak 来检索 对此的弱引用。 只需确认在访问 this 之前能够获取强引用。 同样, get_weakwinrt::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 中使用委托处理事件

上一节强调了协程和并发领域中潜在的生命周期问题。 但是,如果使用对象的成员函数处理事件,或者从对象成员函数内的 lambda 函数内处理事件,则需要考虑事件接收者的相对生存期(处理事件的对象)和事件源(引发事件的对象)。 让我们看看一些代码示例。

下面的代码列表首先定义了一个简单的 EventSource 类,该类会引发由已添加到该类的任何委托处理的泛型事件。 此示例事件恰好使用 Windows::Foundation::EventHandler 委托类型,但此处的问题和补救措施适用于任何和所有委托类型。

然后, EventRecipient 类以 lambda 函数的形式为 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 指针的 lambda 事件处理函数。 每当事件接收者的生命周期长于事件源时,它也会长于这些依赖项。 在这些情况下,这种模式非常有效。 其中一些情况很明显,例如 UI 页面处理由页面上的控件引发的事件时。 页面的生命周期长于按钮,因此事件处理程序的生命周期也长于按钮。 每当收件人拥有源(例如,作为数据成员),或者收件人和源是同级并且直接由其他对象拥有时,这一点就属实。

当你确定存在这样一种情况:处理程序的生命周期不会长于它所依赖的 this,那么你就可以像平常一样捕获 this,而不必考虑强或弱生命周期。

但是,仍然存在一些情况,在这些情况下,this 的生命周期不会持续到处理程序使用它之后(包括处理异步操作和运算引发的完成事件和进度事件的处理程序),因此,了解如何应对这些情况非常重要。

  • 当事件源同步引发事件时,你可以撤销你的处理程序,并确信不会再收到任何事件。 但是,对于异步事件,即使在撤销之后(尤其是在析构函数中撤销时),已在传递途中的事件也可能在对象开始析构后到达该对象。 在销毁前找到执行取消订阅的位置或许可以缓解这个问题,但请继续阅读,了解一种更稳妥的解决方案。
  • 如果你正在编写一个用于实现异步方法的协程,那么这是可行的。
  • 在极少数情况下,对于某些 XAML UI 框架对象(例如 SwapChainPanel),如果接收方对象在未从事件源注销的情况下被终结,就可能会出现这种情况。

问题

此下一版本的 函数模拟当事件接收者被销毁(也许超出范围)时会发生什么情况,而事件源仍在引发事件。

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.
}

事件接收方已被销毁,但其中的 lambda 事件处理程序仍然订阅了 Event 事件。 当该事件被触发时,lambda 会尝试解引用 this 指针,而此时该指针已经无效。 因此,访问冲突是由于处理程序中的代码(或协程的延续部分)试图使用它而导致的。

Important

如果遇到这种情况,那么你就需要考虑 this 对象的生命周期,以及被捕获的 this 对象是否比该捕获存活得更久。 如果没有,请使用强引用或弱引用捕获它,如下所示。

或者——如果这种做法适合你的场景,并且线程方面的考虑也允许这样做——另一种选择是在接收方处理完该事件之后,或在接收方的析构函数中,注销该处理程序。 请参阅 撤销已注册的委托

这就是我们注册处理程序的方式。

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

lambda 通过引用自动捕获任何局部变量。 因此,对于此示例,我们等效地编写了此代码。

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

在这两种情况下,我们只是捕获原始 this 指针。 这不会影响引用计数,因此不会阻止当前对象被销毁。

解决方案

解决方案是捕获一个强引用(或者,正如我们将看到的,如果那样更合适,也可以捕获一个弱引用)。 强引用 递增引用计数,并使当前 对象保持活动 状态。 只需声明一个捕获变量(本例中称为 strong_this),并通过调用 implements::get_strong 来初始化它,以获取我们的 this 指针的强引用。

Important

由于 get_strongwinrt::implements 结构模板的成员函数,因此只能从直接或间接派生自 winrt::implements 的类(如 C++/WinRT 类)调用它。 有关从 winrt::implements 派生的详细信息和示例,请参阅 使用 C++/WinRT 编写 API

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 来检索 对此的弱引用。 弱引用 不会 使当前对象保持活动状态。 因此,只需确认在访问成员之前,仍可以从弱引用中检索强引用。

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;
    }
});

如果捕获原始指针,则需要确保使指向对象保持活动状态。

如果你使用成员函数作为委托

这些原则不仅适用于 lambda 函数,也适用于把成员函数用作委托。 语法不同,因此让我们看看一些代码。 首先,下面是使用原始 指针的可能不安全的成员函数事件处理程序。

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;
    }
};

这是引用对象及其成员函数的标准、常规方法。 为确保安全,从 Windows SDK 10.0.17763.0 版(Windows 10 版本 1809)开始,可以在注册处理程序时建立强引用或弱引用。 此时,已知事件接收者对象仍处于活动状态。

若要获取强引用,只需调用 get_strong 来代替原始的 this 指针。 C++/WinRT 可确保生成的委托保留对当前对象的强引用。

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

捕获强引用意味着,只有在处理程序已取消注册且所有未完成的回调都已返回之后,对象才会被销毁。 但是,该保证仅在引发事件时有效。 如果事件处理程序是异步的,则必须在第一个挂起点之前为协同例程提供对类实例的强引用(有关详细信息和代码,请参阅本主题前面的 类成员协同例程部分中安全访问 指针 )。 但是,这在事件源和对象之间创建循环引用,因此需要通过撤销事件来显式中断该引用。

对于弱引用,请调用 get_weak。 C++/WinRT 可确保生成的委托保存弱引用。 在最后一刻,委托会在幕后尝试将弱引用解析为强引用,并且仅在该操作成功时才调用成员函数。

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

如果委托对象确实调用了你的成员函数,那么 C++/WinRT 将使你的对象保持存活,直到处理函数返回。 但是,如果处理程序是异步的,则它会在挂起点返回,因此必须在第一个挂起点之前为协同例程提供对类实例的强引用。 同样,有关详细信息,请参阅本主题前面的 类成员协同例程部分中安全访问 指针

如果成员函数不属于Windows 运行时类型

如果get_strong方法不可用(类型不是Windows 运行时类型),则可以使用下面的代码示例所示的技术。 此处显示了处理 NetworkInformation.NetworkStatusChanged 事件的常规 C++ 类(名为 ConsoleNetworkWatcher)。

#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 事件,作为弱引用的另一个示例。 该代码使用一个捕获了接收方弱引用的 lambda 表达式注册了一个事件处理程序。

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.
}

在 lambda 捕获子句中,会创建一个临时变量,表示对 this 的弱引用。 在 lambda 表达式的主体中,如果可以获得指向 this 的强引用,则调用 OnCompositionScaleChanged 函数。 这样一来,在 OnCompositionScaleChanged 内部,就可以安全地使用 this

C++/WinRT 中的弱引用

在上文中,我们已经看到弱引用的使用。 一般情况下,它们非常适合打破循环引用。 例如,对于基于 XAML 的 UI 框架的本机实现(由于框架的历史设计),C++/WinRT 中的弱引用机制是处理循环引用所必需的。 不过,在 XAML 之外,你很可能不需要使用弱引用(倒不是说弱引用本身有什么 XAML 特有之处)。 相反,在多数情况下,你应该能够通过合理设计自己的 C++/WinRT API,避免使用循环引用和弱引用。

对于你声明的任何类型,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