类的构造方式
在本文中,我们将了解运行时类的构造方式。我们将在本文中使用以下 Widget 运行时类:
public ref class Widget sealed
{
public:
Widget() : _number(0) { }
Widget(int number) : _number(number) { }
int GetNumber() { return _number; }
private:
int _number;
};
此类型既有默认构造函数,也有带有 int 参数的构造函数。C++/CX 运行时类构造函数与普通 C++ 类类型的构造函数大致相同。与普通成员函数一样,任何属于运行时类公共接口的构造函数只能在其签名中使用 Windows 运行时类型。此规则适用于公共运行时类的公共和受保护构造函数,因为它们构成了运行时类的接口。除此之外,关于运行时类构造函数就没什么好说的了。
C++/CX 添加了一个新的运算符 ref new,用于构造运行时类的实例。例如,我们可以使用其任一构造函数轻松构造 Widget 实例:
Widget^ widgetZero = ref new Widget();
Widget^ widgetAnswer = ref new Widget(42);
ref new 的行为与 new 的行为类似:它接受要构造的运行时类和一组要传递给该运行时类的构造函数的参数,并构造该类型的实例。new T() 生成指向新对象的 T*,而 ref new T() 生成 T^。在这方面,ref new 类似于可用于安全构造 shared_ptr 的 make_shared 辅助函数。
正如我们在之前的文章中看到的那样,除了告诉编译器 Widget 是 Windows 运行时类型的语法标记(例如 ^ 和 ref)之外,此代码看起来几乎与适用于普通 C++ 类型的等效 C++ 代码完全相同。构造函数以相同的方式声明,ref new 的使用方式与 new 的使用方式大致相同。但是,这种语法简单性隐藏了相当多的复杂机制,这正是我们将在这里研究的内容。
由于 C++/CX 隐藏了这里的所有复杂性,因此我们将使用 WRL 来解释对象构造的工作原理。为了有一个起点,我们将 Widget 类型转换为 WRL。首先是声明 Widget 运行时类型及其默认接口 IWidget 的 IDL:
[exclusiveto(Widget)]
[uuid(ada06666-5abd-4691-8a44-56703e020d64)]
[version(1.0)]
interface IWidget : IInspectable
{
HRESULT GetNumber([out] [retval] int* number);
}
[version(1.0)]
runtimeclass Widget
{
[default] interface IWidget;
}
Widget 类型的 C++ 定义如下:
class Widget : public RuntimeClass<IWidget>
{
InspectableClass(RuntimeClass_WRLWidgetComponent_Widget, BaseTrust)
public:
Widget() : _number(0) { }
Widget(int number) : _number(number) { }
STDMETHODIMP GetNumber(int* number) { *number = _number; return S_OK; }
private:
INT32 _number;
};
请注意:为简洁起见,本文中的大多数示例都省略了错误处理代码。在编写实际代码时,请务必处理错误情况,包括空指针和失败的 HRESULT。
虽然此 C++ Widget 类定义了两个构造函数,但这些构造函数是 Widget 类型的实现细节。回想一下第 1 部分,我们只通过接口指针与运行时类对象交互:由于构造函数未由任何接口声明,因此它们不是运行时类的公共接口的一部分。
Widget 来自哪里?
运行时类的结构是用于实现运行时类的特定语言和框架的实现细节;因此,构造此类对象的方式也是实现细节,因为构造与类型的结构密不可分。Windows 运行时组件可从支持 Windows 运行时的任何语言中使用,因此我们需要一种与语言无关的机制来构造运行时类对象。
Windows 运行时使用激活工厂来构造运行时类对象。激活工厂是一个运行时类,其目的是构造特定运行时类类型的对象。我们将定义一个构造 Widget 对象的 WidgetFactory 激活工厂。
与任何其他运行时类一样,激活工厂实现一组接口。每个激活工厂都必须实现 IActivationFactory 接口,该接口声明一个成员函数:ActivateInstance。ActivateInstance 接口函数不接受任何参数并返回默认构造的对象。激活工厂还可以实现定义其他“构造”函数的用户定义的工厂接口。对于我们的 WidgetFactory,我们将使用以下工厂接口:
[exclusiveto(Widget)]
[uuid(5b197688-2f57-4d01-92cd-a888f10dcd90)]
[version(1.0)]
interface IWidgetFactory : IInspectable
{
HRESULT CreateInstance([in] int value, [out] [retval] Widget** widget);
}
工厂接口只能声明工厂函数:每个函数必须接受一个或多个参数,并且必须返回运行时类的实例。Widget 运行时类只有一个非默认构造函数,因此我们只需在此处声明一个工厂函数,但可以根据需要定义任意多个工厂函数。使用 C++/CX 时,编译器会自动为每个公共 ref 类生成一个工厂接口,其中工厂函数的参数与 ref 类的每个构造函数的参数相对应。
除了为 Widget 类型定义工厂接口外,我们还需要在 IDL 中将其注释为可激活。我们使用 activatable 属性来执行此操作,该属性有两种形式,我们将这两种形式都用于我们的 Widget 类型:
[activatable(1.0)]
[activatable(IWidgetFactory, 1.0)]
第一种形式将类型声明为默认可构造。第二种形式声明 IWidgetFactory 是运行时类的工厂接口。当 midlrt 编译器将 IDL 文件编译为 Windows 元数据 (WinMD) 文件时,它将使用这些属性将正确的构造函数集添加到运行时类的元数据中。
接下来,我们需要实现一个同时实现 IActivationFactory 和 IWidgetFactory 接口的 WidgetFactory 类型。我们将使用 ActivationFactory 基类模板(旨在支持激活工厂),而不是使用 WRL RuntimeClass 基类模板。
class WidgetFactory : public ActivationFactory<IWidgetFactory>
{
InspectableClassStatic(RuntimeClass_WRLWidgetComponent_Widget, BaseTrust)
public:
STDMETHODIMP ActivateInstance(IInspectable** widget) override
{
*widget = Make<Widget>().Detach();
return *widget != nullptr ? S_OK : E_OUTOFMEMORY;
}
STDMETHODIMP CreateInstance(int value, IWidget** widget) override
{
*widget = Make<Widget>(value).Detach();
return *widget != nullptr ? S_OK : E_OUTOFMEMORY;
}
};
ActivationFactory 提供了 IActivationFactory 接口的默认实现;此默认实现只是将 ActivateInstance 定义为返回 E_NOTIMPL。这适用于不可默认构造的运行时类;对于可默认构造的运行时类(如 Widget),我们需要重写 ActivateInstance 以实际默认构造一个对象。
Make<Widget>() 实际上等同于 new (nothrow) Widget():它动态地为 Widget 分配内存并将提供的参数传递给 Widget 构造函数。与 new (nothrow) 一样,如果分配失败,它会生成 nullptr,请记住,我们不能从实现接口的函数中抛出异常,我们必须返回 HRESULT。它返回 ComPtr<Widget>;由于我们返回的是接口指针,我们只需分离指针并返回它,调用者负责对所有返回的接口指针调用 Release。
这就是我们实现 WidgetFactory 激活工厂所需的全部内容。如果我们可以得到工厂的实例,我们就可以轻松创建Widget对象。例如,
void Test(ComPtr<IWidgetFactory> const& factory)
{
ComPtr<IWidget> widget;
factory->CreateInstance(42, widget.GetAddressOf());
// Hooray, we have a widget!
}