C++的/CX第2部分:戴帽子的类型

看到了吗 C++ [CX]第0部分[N]简介 作为本系列的介绍。

null

帽子( ^ 是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 IGetValueISetValue    {    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<__INumberPublicNonVirtualsconstnumberIf)    {        // 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(Numbernumber)    {        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(Numbernumber)    {        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(Numbernumber)    {        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* 必要时,可大大减少编程错误的机会。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享