命名空间: 具现 类型、实现类型和工厂
如这个系列前面的内容所示,C++/WinRT 运行时类在多个命名空间中以多个 C++ 类的形式存在。 因此,名称 MyRuntimeClass 在 winrt::MyProject 命名空间中有一种含义,在 winrt::MyProject::implementation 命名空间中有另一种含义。 如果需要一个来自其他命名空间的名称,请注意你目前的上下文中有哪一个命名空间,然后使用命名空间前缀。 让我们进一步了解所讨论的命名空间。
winrt::MyProject。 此命名空间包含 具现 类型。 具现 类型的对象是一个代理,它实质上是一个指向后备对象的智能指针,该后备对象可能会在你的项目中的此处实现,也可能在另一个编译单元中实现。
winrt::MyProject::implementation。 此命名空间包含实现类型。 实现类型的对象不是指针;它是一个值,一个完整的 C++ 堆栈对象。 不要直接构造实现类型;而应调用 winrt::make,将实现类型作为模板参数传递。 在本主题前面的部分中,我们已演示了实际发挥作用的 winrt::make 示例,并且 XAML 控制;绑定到 C++/WinRT 属性中还有另一个示例。 另请参阅 Diagnosing direct allocations(诊断直接分配)。
winrt::MyProject::factory_implementation。 此命名空间包含工厂。 此命名空间中的对象支持 IActivationFactory。
下表显示了需要在不同的上下文中使用的最小命名空间限定。
上下文的命名空间 | 指定具现类型 | 指定实现类型 |
winrt::MyProject | MyRuntimeClass | implementation::MyRuntimeClass |
winrt::MyProject::implementation | MyProject::MyRuntimeClass | MyRuntimeClass |
当想要从实现中返回 具现 类型时,请注意不要通过编写 MyRuntimeClass myRuntimeClass; 来实例化实现类型。 本文中前面的实例化和返回实现类型和接口部分显示了适用于该情况的正确技术和代码。
在这种情况下,MyRuntimeClass myRuntimeClass; 存在的问题是,它在堆栈上创建 winrt::MyProject::implementation::MyRuntimeClass 对象。 实现类型的该对象的行为在某些方面类似于 具现 类型,你可以采用相同的方式调用它的方法;并且它甚至会转换为 具现 类型。 但作用域退出时,该对象将按照正常 C++ 规则析构。 因此,如果向该对象返回了 具现 类型的智能指针,那么该指针现在无关联。
此内存损坏类型的 bug 很难诊断。 因此,对于调试版本,C++/WinRT 断言可帮助你使用堆栈检测器捕获此错误。 但是,协同例程是在堆上分配的,因此如果让此错误出现在协同例程内,那么你不会得到有关此错误的帮助。
选择加入统一构造和直接实现访问(上)
此部分介绍一项可以选择加入的 C++/WinRT 2.0 功能,不过在新项目中,该功能是默认启用的。 对于现有项目,需要通过配置 cppwinrt.exe 工具来选择加入。 在 Visual Studio 中,将项目属性“常见属性” >“C++/WinRT” >“已优化” 设置为“是” 。 该操作的效果是将 <CppWinRTOptimized>true</CppWinRTOptimized> 添加到项目文件。 它与从命令行调用 cppwinrt.exe 时添加开关具有相同的效果。
-opt[imize] 开关启用通常称为“统一构造”的功能。 有了统一(或统一)构造,就可以通过 C++/WinRT 语言 具现 本身高效地创建并使用实现类型(通过组件实现的供应用程序使用的类型),不会造成任何加载器困难。
在介绍此功能之前,让我们先演示一下没有统一构造的情况。 我们将从下面这个示例性的 Windows 运行时类着手进行演示。
// MyClass.idl
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void Method();
static void StaticMethod();
}
}
作为熟悉 C++/WinRT 库的使用的 C++ 开发人员,你可能希望像下面这样使用该类。
using namespace winrt::MyProject;
MyClass c;
c.Method();
MyClass::StaticMethod();
如果所显示的使用代码并没有驻留在用于实现该类的组件中,则这样做很合理。 充当语言 具现 的 C++/WinRT 会将作为开发人员的你与 ABI(基于 COM 的应用程序二进制接口,由 Windows 运行时定义)隔离开来。 C++/WinRT 不直接调用实现,而是通过 ABI 来调用。
因此,在构造 MyClass 对象 (MyClass c;) 的代码行中,C++/WinRT 具现 会调用 RoGetActivationFactory 来检索类或激活工厂,然后使用该工厂来创建对象。 最后一行同样使用该工厂进行调用(看起来是静态方法调用)。 所有这些都需要你的类进行注册并且你的模块实现 DllGetActivationFactory 入口点。 C++/WinRT 有速度很快的工厂缓存,因此,对于使用你的组件的应用程序来说,这些都不会造成问题。 问题在于,在你的组件中,你刚才的操作有些问题。
首先,不管 C++/WinRT 工厂缓存有多快,通过 RoGetActivationFactory 进行调用的速度(或者甚至包括通过工厂缓存进行后续调用的速度)始终会慢于直接调用实现的速度。 很明显,对于本地定义的类型来说,依次调用 RoGetActivationFactory、IActivationFactory::ActivateInstance、QueryInterface 的效率不如使用 C++ new 表达式的效率高。 因此,在组件中创建对象时,有经验的 C++/WinRT 开发人员习惯于使用 winrt::make 或 winrt::make_self 帮助程序函数。
// MyClass c;
MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
但是,如你所见,这根本谈不上方便,也不简洁。 必须使用帮助程序函数来创建对象,还必须在实现类型和具现类型之间进行区分。
其次,使用具现来创建类意味着会缓存其激活工厂。 通常情况下,这是你希望的,但如果工厂驻留在进行调用的模块 (DLL) 中,则必须有效地固定 DLL 并防止其卸载。 许多情况下,这没有关系,但某些系统组件必须支持卸载。
这种情况下,我们有必要谈一谈“统一构造”这个术语。 不管创建代码是驻留在只使用类的项目中,还是驻留在对类进行实际实现的项目中, 你都可以自由地使用相同语法来创建对象。
// MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
MyClass c;
使用 -opt[imize] 开关生成组件项目时,通过语言具现进行的调用会编译成对 winrt::make 函数的同一高效调用,该函数会直接创建实现类型。 这样会使得你的语法既简单又可以预测,避免了通过工厂进行调用带来的性能冲击,不需在此过程中固定组件。 除了组件项目,这还适用于 XAML 应用程序。 为同一应用程序中实现的类绕过 RoGetActivationFactory 以后,就可以构造这些类(无需注册),所采用的方式与这些类位于组件之外时所采用的方式完全相同。
统一构造适用于由工厂在后台进行的任何调用。 实际上,这意味着优化是同时针对构造函数和静态成员的。 下面还是该原始示例。
MyClass c;
c.Method();
MyClass::StaticMethod();
在没有 -opt[imize] 的情况下,第一个和最后一个语句需要通过工厂对象进行调用。 如果使用 -opt[imize],则这两个语句都不需要那样进行调用。 这些调用是直接针对实现进行编译的,甚至可以使用内联的方式。 这就涉及到在讨论 -opt[imize] 时通常会用到的另一术语,即“直接实现”访问。
语言 具现 是方便,但当你可以直接访问实现时,你可以而且应该利用直接访问,尽可能生成最有效的代码。 C++/WinRT 可以为你实现这一点,不会强制要求你舍弃 具现 的安全性和工作效率。
这是一项重大更改,因为组件必须“合作”,让语言 具现 进入并直接访问其实现类型。 由于 C++/WinRT 是一个仅头文件库,因此你可以在其中查看具体信息。 在没有 -opt[imize] 的情况下,可以通过 具现 将 MyClass 构造函数和 StaticMethod 成员定义如下。
namespace winrt::MyProject
{
inline MyClass::MyClass() :
MyClass(impl::call_factory<MyClass>([](auto&& f){
return f.template ActivateInstance<MyClass>(); }))
{
}
inline void MyClass::StaticMethod()
{
impl::call_factory<MyClass, MyProject::IClassStatics>([&](auto&& f) {
return f.StaticMethod(); });
}
}
没有必要完全遵循上面的代码;该代码的目的是为了表明两个调用都涉及调用名为 call_factory 的函数。 你可以据此认为,这些调用涉及工厂缓存,不直接访问实现。 如果使用 -opt[imize],则根本就不定义这些函数, 而是通过 具现 来声明它们,其定义留给组件。
然后,组件就可以提供定义来直接调用实现。 现在,我们谈谈重大更改。 这些定义是在你使用 -component 和 -opt[imize] 时为你生成的,它们出现在名为 Type.g.cpp 的文件中,其中的 Type 是所实现的运行时类的名称。 这是你首次在现有项目中启用 -opt[imize] 时可能会遇到各种链接器错误的原因。 若要修复错误,需要将该生成的文件包括到实现中。