看到了吗 C++ [CX]第0部分[N]简介 作为本系列的介绍。
帽子( ^
是C++/CX最突出的特性之一,很难在第一次看到C++/CX代码时注意到它。那么,到底什么是 ^
类型?hat类型是一种智能指针类型,它(1)自动管理Windows运行时对象的生存期,(2)提供自动类型转换功能以简化Windows运行时对象的使用。
我们将通过讨论如何通过WRL使用Windows运行时对象,然后解释C++/CX HAT如何使事情变得简单。出于演示目的,我们将使用 Number
我们在第1部分中介绍的类:
public interface struct IGetValue { int GetValue() = 0; };
public interface struct ISetValue { void SetValue(int value) = 0; };
public ref class Number sealed : public IGetValue, ISetValue { public: Number() : _value(0) { }
virtual int GetValue() { return _value; } virtual void SetValue(int value) { _value = value; }
private:
int _value; };
在此修改 Number
我们定义了一对接口, IGetValue
和 ISetValue
,声明 Number
; Number
然后实现这两个接口。否则,事情看起来应该很熟悉。
请注意 Number
实际上实现了三个Windows运行时接口:除了 IGetValue
和 ISetValue
,编译器仍然生成 __INumberPublicNonVirtuals
接口,其中 Number
工具。因为 Number
由显式实现的接口声明( IGetValue
和 ISetValue
),编译器生成 __INumberPublicNonVirtuals
不声明任何成员。不过,这个接口仍然是必需的,因为它是 默认接口 对于 Number
类型。每个运行时类型都必须有一个默认接口,并且默认接口对于类几乎总是唯一的。稍后我们将了解为什么默认接口很重要。
生命周期管理
Windows运行时引用类型使用引用计数进行对象生存期管理。所有Windows运行时接口(包括 Number
)直接从 IInspectable
接口,它本身派生自COM IUnknown
接口。 IUnknown
声明三个成员函数,用于控制对象的生存期并允许类型转换。
MSDN文章 “管理引用计数的规则” 全面了解 IUnknown
终身管理工作。不过,这些原则非常简单:无论何时创建对对象的新引用,都必须调用 IUnknown::AddRef
增加其引用计数;无论何时“销毁”对对象的引用,都必须调用 IUnknown::Release
减少引用计数。引用计数初始化为零,并在对 AddRef
和 Release
,当引用计数再次达到零时,对象将销毁自身。
当然,在C++编程时,我们应该很少——实际上从不调用。 AddRef
和 Release
直接。相反,我们应该尽可能使用智能指针,在需要时自动进行这些调用。使用智能指针有助于确保对象不会因丢失而泄漏 Release
也不会因为过早的 Release
或未能 AddRef
.
ATL包括 CComPtr
以及一系列相关的智能指针,它们长期以来一直用于COM编程,用于自动管理实现的对象的引用计数 IUnknown
. WRL包括 ComPtr
,这是一个改进和现代化的 CComPtr
(改进示例: ComPtr
不会使一元数过载 &
喜欢 CComPtr
是的)。
对于那些没有做过很多COM编程和不熟悉 ComPtr
s:如果你用过 shared_ptr
(包含C++ 11、C++ Tr1和Boost)的一部分, ComPtr
在生命周期管理方面具有有效的相同行为。机制不同( ComPtr
利用 IUnknown
,而 shared_ptr
支持任意类型,因此必须使用外部引用计数),但生存期管理行为是相同的。
C++/CX HAT具有完全相同的生命周期管理语义 ComPtr
. 当 T^
是复制的, AddRef
调用以增加引用计数 T^
超出范围或被重新分配, Release
调用以减少引用计数。我们可以考虑一个简单的示例来演示引用计数行为:
{ T^ t0 = ref new A(); T^ t1 = ref new B();
t0 = t1; t0 = nullptr; }
首先,我们创建一个 A
对象并将其所有权授予 t0
. 此的引用计数 A
对象是1,因为有一个 T^
这是有参考价值的。然后我们创建一个 B
对象并将其所有权授予 t1
. 此的引用计数 B
对象也是1。
最终的结果 t0 = t1
两者都是吗 t0
和 t1
指向同一个对象。这必须分三步进行。第一, t1->AddRef()
调用以增加 B
对象,因为 t1
正在获取对象的所有权。其次, t0->Release()
被召唤释放 t0
的所有权 A
对象。这将导致 A
对象降为零 A
物体会自我毁灭。这将导致 B
目标增加到2。三分之一,最后, t1
设置为指向 B
对象。
然后我们分配 t0 = nullptr
. 这个“重置” t0
为null,这将导致它释放对 B
对象。这叫 t0->Release()
,导致 B
对象减少到1。
最后,执行将到达块的右括号: }
. 在这一点上,所有的局部变量被破坏,顺序相反。第一, t1
已销毁(智能指针,而不是指向的对象)。这叫 t1->Release()
,导致 B
对象降到零,所以 B
物体会自我毁灭。 t0
然后被销毁,这是一个不操作,因为它是空的。
如果终身管理是我们唯一关心的问题,那么就不需要真正的 ^
完全: ComPtr<T>
足以管理对象生存期。
类型转换
在C++中,涉及类型的一些类型转换是隐式的;其他的可以使用一个或一系列的铸型来执行。例如,如果 Number
它实现的接口是普通C++类型而不是Windows运行时类型,转换为 Number*
到 IGetValue*
我们可以从 IGetValue*
到 Number*
使用 static_cast
或者 dynamic_cast
.
这些转换不适用于Windows运行时引用类型,因为引用类型的实现是不透明的,并且未指定内存中引用类型的布局。在C语言中实现的引用类型可以与C++中实现的等效引用类型不同。因此,我们不能依赖于C++语言特定的特性,如隐式派生到基转换和直接在Windows运行时类型中工作时的转换。
为了执行这些转换,我们必须使用 IUnknown
接口: IUnknown::QueryInterface
. 这个成员函数可以看作是语言中立的 dynamic_cast
:它尝试执行到指定接口的转换,并返回转换是否成功。因为每个运行时类型都实现 IUnknown
接口,并为 QueryInterface
,它可以执行所需的任何操作,以在实现它的语言和框架中获得正确的接口指针。
通过WRL使用对象
让我们看看如何使用 Number
通过WRL上课。此示例接受 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); }
这个 As
WRL的成员函数模板 ComPtr
简单地封装对 IUnknown::QueryInterface
以一种有助于防止常见编程错误的方式。我们先用这个来得到 ISetValue
要调用的接口指针 SetValue
然后再次得到 IGetValue
要调用的接口指针 GetValue
.
如果我们得到一个 Number*
打电话来了 SetValue
和 GetValue
通过那个指针。不幸的是,我们不能这样做:回想一下,引用类型的实现是不透明的,我们只通过指向它实现的接口之一的指针与对象交互。这意味着我们永远不会有一个 Number*
; 真的没有这回事。相反,我们只能参考 Number
通过 IGetValue*
,安 ISetValue*
,或 __INumberPublicNonVirtuals*
.
仅仅调用两个成员函数的代码就太多了,这个示例演示了我们必须克服的关键障碍之一,以便更容易地使用Windows运行时类型。与COM不同,Windows运行时不允许从另一个Windows运行时接口派生接口;所有接口必须直接从 IInspectable
. 每个接口都是独立的,我们只能通过接口与一个对象交互,因此如果我们使用的是实现多个接口的类型(许多类型都是这样),那么我们就不得不编写大量相当冗长的类型转换代码,这样我们就可以获得正确的接口指针来进行每个函数调用。
通过C++实现对象的使用
C++的主要优点之一是编译器知道哪些类型是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
也和我们期望的一样:例如,我们可以修改这个例子来获得 IGetValue^
从 ISetValue^
:
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++(C++/Cx)所使用的类型(在全局命名空间下定义的类型)的“高级”定义冲突。
除了它的类型转换功能外,hat还提供了其他的好处,这些好处是通过使用普通的智能指针(如 ComPtr
. 其中一个最重要的好处是帽子可以在任何地方统一使用。一个以接口指针作为参数的成员函数被声明为一个原始指针(这是Windows运行时ABI的一部分,它被设计成简单的和语言无关的,因此不知道C++智能指针是什么)。所以,当一个人可以使用 ComPtr
在大多数情况下,原始指针仍然需要在ABI边界处使用,并且存在细微(不太细微)编程错误的空间。
使用C++ +CX,编译器已经转换成员函数签名,以便在异常和HEST之间进行转换,编译器也能够从 T^
到 T*
必要时,可大大减少编程错误的机会。