winrt::implements 结构模板是你自己的 C++/WinRT 实现(运行时类和激活工厂)直接或间接派生的基础。
本主题讨论 C++/WinRT 2.0 中 winrt::implements 的扩展点。 你可以选择在你的实现类型中实现这些扩展点,以便自定义可检查对象的默认行为(这里的 inspectable 是指 IInspectable 接口意义上的可检查对象)。
这些扩展点允许你延后实现类型的销毁、在销毁过程中安全地进行查询,并挂接到投影方法的进入和退出点。 本主题介绍这些功能,并详细介绍何时以及如何使用这些功能。
延迟销毁
在 “诊断直接分配 ”主题中,我们提到你的实现类型不能有专用析构函数。
拥有公共析构函数的好处在于,它支持延迟销毁,也就是说,可以检测对你的对象发出的最后一次 IUnknown::Release 调用,然后接管该对象的所有权,从而将其销毁无限期延后。
回想一下,经典 COM 对象在内部引用计数;引用计数通过 IUnknown::AddRef 和 IUnknown::Release 函数进行管理。 在 Release 的传统实现中,一旦引用计数达到 0,就会调用经典 COM 对象的 C++ 析构函数。
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
delete this; 会在释放该对象占用的内存之前调用该对象的析构函数。 这非常有效,前提是你不需要在析构函数中执行任何有趣的操作。
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
我们的意思是 有趣的是什么? 首先,析构函数本质上是同步的。 无法切换线程 , 也许要在不同的上下文中销毁某些特定于线程的资源。 无法可靠地查询对象以获取可能需要的一些其他接口,以便释放某些资源。 该列表继续。 对于销毁操作较为复杂的情况,需要采用更灵活的解决方案。 这是 C++/WinRT 的 final_release 函数传入的位置。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
// This is the first stop...
}
~Sample() noexcept
{
// ...And this happens only when *unique_ptr* finally deletes the object.
}
};
我们更新了 Release 的 C++/WinRT 实现,以在对象的引用计数转换为 0 时立即调用 final_release 。 在该状态下,对象可以确信没有进一步未完成的引用,并且它现在拥有自身的独占所有权。 因此,它可以将自身所有权转让给静态 final_release 函数。
换句话说,该对象已从支持共享所有权的对象转变为独占所有的对象。 std::unique_ptr具有对象的独占所有权,因此,当 std::unique_ptr超出范围时,它自然会销毁对象作为其语义的一部分(因此需要公共析构函数)(前提是它以前没有移动到其他位置)。 这就是关键。 可以无限期地使用该对象,前提是 std::unique_ptr 使对象保持活动状态。 下面是有关如何将对象移到其他位置的插图。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
batch_cleanup.push_back(std::move(ptr));
}
};
此代码将对象保存在名为 batch_cleanup 其作业之一的集合中,该集合将在应用运行时的某个将来点清理所有对象。
通常,对象会在 std::unique_ptr 析构时析构,但你也可以通过调用 std::unique_ptr::reset 让它提前析构;或者通过将 std::unique_ptr 保存在某处来推迟其析构。
也许更实际和更强大,你可以将 final_release 函数转换为协同例程,并在一个位置处理其最终销毁,同时能够根据需要暂停和切换线程。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static winrt::fire_and_forget final_release(std::unique_ptr<Sample> ptr) noexcept
{
co_await winrt::resume_background(); // Unwind the calling thread.
// Safely perform complex teardown here.
}
};
挂起点会导致调用线程(即最初发起对 IUnknown::Release 函数调用的线程)返回,从而向调用方表明,它先前持有的对象已无法再通过该接口指针访问。 UI 框架通常需要确保对象在最初创建该对象的特定 UI 线程上销毁。 此功能使满足此类要求变得微不足道,因为销毁与释放对象分离。
请注意,传递给 final_release 的对象只是 C++ 对象;它不再是 COM 对象。 例如,对对象的现有 COM 弱引用不再解析。
销毁期间的安全查询
基于延迟销毁这一概念,还可以在销毁过程中安全地查询接口。
经典 COM 基于两个中心概念。 第一个是引用计数,第二个是查询接口。 除了 AddRef 和 Release,IUnknown 接口还提供 QueryInterface。 某些 UI 框架(如 XAML)大量使用该方法来遍历 XAML 层次结构,因为它模拟其可组合类型系统。 请考虑一个简单的示例。
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
这 似乎无害 。 此 XAML 页面希望在析构函数中清除其数据上下文。 但 DataContext 是 FrameworkElement 基类的属性,它位于不同的 IFrameworkElement 接口上。 因此,C++/WinRT 必须注入对 QueryInterface 的调用,才能查找正确的 vtable,然后才能调用 DataContext 属性。 但是,我们之所以会进入析构函数,是因为引用计数已经变为 0。 在此处调用 QueryInterface 会暂时增加引用计数;当该计数再次降为 0 时,对象会再次析构。
C++/WinRT 2.0 已强化以支持此功能。 下面是 Release 的 C++/WinRT 2.0 实现的简化版本。
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
正如你可能预测的那样,它首先递减引用计数,然后仅在没有未完成的引用时才执行。 但是,在调用本主题前面所述的静态 final_release 函数之前,它会通过将引用计数设置为 1 来稳定引用计数。 我们将其称为 去抖(借用电气工程中的术语)。 这对于防止最终引用被发布至关重要。 发生这种情况后,引用计数不稳定,并且无法可靠地支持对 QueryInterface 的调用。
在发布最终引用后调用 QueryInterface 是危险的,因为引用计数随后可以无限期增长。 你有责任只调用不会延长对象的生存期的已知代码路径。 C++/WinRT 会帮你把这件事做到一半,确保这些 QueryInterface 调用可以可靠地执行。
它通过稳定引用计数来执行该操作。 当最后一个引用被释放后,实际引用计数要么为 0,要么为某个完全不可预测的值。 如果涉及弱引用,则后一种情况可能会发生。 无论哪种方式,如果随后调用 QueryInterface,这种做法就无法维持;因为这必然会导致引用计数暂时增加,这也就是提到“去抖动”的原因。 将其设置为 1 可确保对 Release 的最终调用永远不会在此对象上再次发生。 这正是我们想要的,因为 std::unique_ptr 现在拥有该对象的所有权,而对 QueryInterface/Release 的成对调用只要次数有限,就是安全的。
请考虑一个更有趣的示例。
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
static winrt::fire_and_forget final_release(std::unique_ptr<MainPage> ptr)
{
co_await 5s;
co_await winrt::resume_foreground(ptr->DispatcherQueue());
ptr = nullptr;
}
};
首先,调用 final_release 函数,通知实现是时候清理了。 在这里,final_release 恰好是一个协程。 为了模拟第一个挂起点,它会先在线程池中等待几秒钟。 然后,它会在页面的调度程序队列线程上恢复。 最后一步涉及查询,因为 DispatcherQueue 可从 DependencyObject 基类访问。 最后,通过将 nullptr 赋值给 std::unique_ptr,该页面实际上会被删除。 这进而会调用该页面的析构函数。
在析构函数中,我们清除数据上下文;我们知道,这需要 对 FrameworkElement 基类进行查询。
这一切之所以成为可能,是因为 C++/WinRT 2.0 提供了引用计数去抖(或引用计数稳定化)机制。
方法进入和退出钩子
不太常用的扩展点是 abi_guard 结构,以及 abi_enter 和 abi_exit 函数。
如果你的实现类型定义了函数 abi_enter,则该函数会在你的每个投影接口方法的入口处被调用(不包括 IInspectable 的方法)。
同样,如果定义 abi_exit,则会在退出每个此类方法时调用:但是,如果 abi_enter 引发异常,则不会调用它。 即使你的投影接口方法本身引发了异常,它仍会被调用。
例如,如果客户端尝试在对象已处于不可用状态后仍使用它——比如在调用 ShutDown 或 Disconnect 方法之后——则可以使用 abi_enter 引发一个假设的 invalid_state_error 异常。 如果基础集合已更改,C++/WinRT 迭代器类使用此功能在 abi_enter 函数中引发无效的状态异常。
在简单 abi_enter 和 abi_exit函数的上方,可以定义名为 abi_guard的嵌套类型。 在这种情况下,在进入你的投影接口的每个(非 IInspectable)方法时,都会创建一个 abi_guard 实例,并将该对象的引用作为其构造函数参数。 随后,abi_guard 会在退出该方法时析构。 可以将你喜欢的任何额外状态放入 abi_guard 类型。
如果你没有定义自己的 abi_guard,那么会使用一个默认实现:它在构造时调用 abi_enter,并在析构时调用 abi_exit。
仅当 通过投影接口调用方法时,才使用这些防护。 如果直接在实现对象上调用方法,则这些调用将直接转到实现,没有任何防护。
下面是一个代码示例。
struct Sample : SampleT<Sample, IClosable>
{
void abi_enter();
void abi_exit();
void Close();
};
void example1()
{
auto sampleObj1{ winrt::make<Sample>() };
sampleObj1.Close(); // Calls abi_enter and abi_exit.
}
void example2()
{
auto sampleObj2{ winrt::make_self<Sample>() };
sampleObj2->Close(); // Doesn't call abi_enter nor abi_exit.
}
// A guard is used only for the duration of the method call.
// If the method is a coroutine, then the guard applies only until
// the IAsyncXxx is returned; not until the coroutine completes.
IAsyncAction CloseAsync()
{
// Guard is active here.
DoWork();
// Guard becomes inactive once DoOtherWorkAsync
// returns an IAsyncAction.
co_await DoOtherWorkAsync();
// Guard is not active here.
}