通过 WRL 使用对象
让我们看看如何通过 WRL 使用我们的 Number 类。此示例接受 Number 的实例并调用 SetValue 来设置值,并调用 GetValue 来获取值。(为简洁起见,省略了错误检查。)
void F(ComPtr<__INumberPublicNonVirtuals> const& numberIf)
{
// Get a pointer to the object's ISetValue interface and set the value:
ComPtr<ISetValue> setValueIf;
numberIf.As(&setValueIf);
setValueIf->SetValue(42);
// Get a pointer to the object's IGetValue interface and get the value:
ComPtr<IGetValue> getValueIf;
numberIf.As(&getValueIf);
int value = 0;
getValueIf->GetValue(&value);
}
WRL ComPtr 的 As 成员函数模板只是以一种有助于防止常见编程错误的方式封装了对 IUnknown::QueryInterface 的调用。我们首先使用它来获取 ISetValue 接口指针以调用 SetValue,然后再次获取 IGetValue 接口指针以调用 GetValue。
如果我们获得一个 Number* 并通过该指针调用 SetValue 和 GetValue,这会简单得多。不幸的是,我们无法做到这一点:回想一下,引用类型的实现是不透明的,我们只能通过指向它实现的接口之一的指针与对象交互。这意味着我们永远无法拥有 Number*;实际上没有这样的东西。相反,我们只能通过 IGetValue*、ISetValue* 或 __INumberPublicNonVirtuals* 引用 Number 对象。
只是为了调用两个成员函数,这需要大量的代码,这个示例演示了我们必须克服的一个关键障碍,以便更轻松地使用 Windows 运行时类型。与 COM 不同,Windows 运行时不允许接口从另一个 Windows 运行时接口派生;所有接口都必须直接从 IInspectable 派生。每个接口都是独立的,我们只能通过接口与对象交互,因此如果我们使用实现多个接口的类型(许多类型都是这样),我们就不得不编写大量相当冗长的类型转换代码,以便获得正确的接口指针来进行每个函数调用。
通过 C++/CX 使用对象
C++/CX 的一个主要优势是编译器知道哪些类型是 Windows 运行时类型。它可以访问定义每个接口和运行时类型的 Windows 元数据 (WinMD) 文件,因此,除其他事项外,它知道每个运行时类型实现的接口集。例如,编译器知道 Number 类型实现了 ISetValue 和 IGetValue 接口,因为元数据指定它确实实现了。编译器能够使用此类型信息自动生成类型转换代码。
考虑以下 C++/CX 示例,它相当于我们介绍的 WRL 示例:
void F(Number^ number)
{
ISetValue^ setValueIf = number;
setValueIf->SetValue(42);
IGetValue^ getValueIf = number;
int value = getValueIf->GetValue();
}
因为编译器知道 Number 类型实现了 ISetValue 和 IGetValue 接口,所以它允许从 Number^ 到 ISetValue^ 和 IGetValue^ 的隐式转换。这种隐式转换会导致编译器生成对 IUnknown::QueryInterface 的调用以获取正确的接口指针。除了更简洁的语法之外,这里真的没有什么神奇之处:编译器只是生成了我们原本必须自己编写的类型转换代码。
dynamic_cast 的工作原理也与我们预期的一样:例如,我们可以修改此示例以从 ISetValue^ 获取 IGetValue^:
void F(Number^ number)
{
ISetValue^ setValueIf = number;
setValueIf->SetValue(42);
IGetValue^ getValueIf = dynamic_cast<IGetValue^>(setValueIf);
int value = getValueIf->GetValue();
}
此示例的行为与第一个例子相同,我们只是采取了不同的步骤来获得相同的行为。如果转换失败,dynamic_cast 可以返回 nullptr(尽管我们知道在这种特定情况下它会成功)。C++/CX 还提供了 safe_cast,如果转换失败,它会引发 Platform::InvalidCastException 异常。
当我们讨论上面的 WRL 示例时,我们注意到没有 Number* 这样的东西:我们只使用接口指针。这引出了一个问题:什么是 Number^?在运行时,Number^ 是 __INumberPublicNonVirtuals^。引用运行时类型(而不是接口)的帽子实际上包含指向该运行时类型的默认接口的指针。
但是,在编译时,编译器将 Number^ 视为引用整个 Number 对象。编译器聚合 Number 实现的所有接口的所有成员,并允许通过 Number^ 直接调用所有这些成员。我们可以使用 Number^ 就像它是 IGetValue^ 或 ISetValue^ 一样,编译器将向 QueryInterface 注入所需的调用以执行函数调用所需的转换。
因此,我们可以进一步缩短我们的 C++/CX 程序:
void F(Number^ number)
{
number->SetValue(42);
int value = number->GetValue();
}
此代码与我们的第一个 C++/CX 示例和 WRL 示例执行的操作完全相同。仍然没有什么神奇之处:编译器只是生成所有样板代码来执行每次函数调用所需的类型转换。
您可能已经注意到,此示例比我们开始使用的 WRL 示例短得多,也简洁得多。所有样板代码都消失了,我们留下的代码除了 ^ 和 ref 告诉编译器我们正在处理 Windows 运行时类型,看起来与与普通 C++ 类型交互的类似 C++ 代码完全一样。但关键在于:理想情况下,我们使用 Windows 运行时类型的代码应尽可能与使用 C++ 类型的代码相似。
ComPtr<T> 和 T^ 都是“无开销”智能指针:每个指针的大小与普通指针的大小相同,使用它们的操作不会做任何不必要的工作。如果您需要在使用 C++/CX 的代码和使用 WRL 的代码之间进行互操作,您可以简单地使用 reinterpret_cast 将 T^ 转换为 T*:
ABI::ISetValue* setValuePtr = reinterpret_cast(setValueIf);
类型的 ABI 级别定义在 ABI 命名空间下的命名空间中定义,因此它们不会与 C++/CX 使用的类型的“高级”定义相冲突,后者在全局命名空间下定义。
除了类型转换功能外,hat 还提供了其他好处,而这些好处无法通过使用普通智能指针,如 ComPtr来实现。其中最重要的一个好处是,hat 可以在任何地方统一使用。以接口指针为参数的成员函数被声明为采用原始指针,这是 Windows 运行时 ABI 的一部分,它设计为简单且与语言无关,因此不知道 C++ 智能指针是什么。因此,虽然可以在大多数地方使用 ComPtr,但原始指针仍然需要在 ABI 边界上使用,并且存在细微和不那么细微编程错误的空间。
使用 C++/CX,编译器已经转换成员函数签名以在异常和 HRESULT 之间进行转换,并且编译器还能够在需要时注入从 T^ 到 T* 的转换,从而大大减少了编程错误的机会。