Visual C++ 2015 是 C++ 团队付出巨大努力将现代C++引入windows平台的成果。在最新的几个发行版本里,VC++已经逐步添加了现代C++语言以及库的特色,这些结合在一起会创造一个用于构建通用windows App和组件的绝对惊艳的开发环境。Visual C++2015建立在早期版本引入的惊人进步,提供了成熟的、支持大多数C++11特性以及C++ 2015子集的编译器。你或许会怀疑编译器支持的完整程度,公正地说,我认为他能支持大部分重要的语言特性,支持现代C++将会迎来windows 程序库开发一片新的天地。这才是关键。只要编译器支持一个高效优雅的库的开发环境,开发者就能构建伟大的app和组件。
这里我不会让你看一个枯燥的新特性列表,或者走马观花地看下它的功能,而是会带你浏览下一些传统情况下的复杂代码现在如何让人相当愉快书写。当然,这得益于成熟的Visual C++编译器。我将会向你展示windows的一些本质,在现在或将来API中实际上都是很重要的本质。
颇具讽刺意味的是,对于COM来说,C++已经足够现代了.是的,我在谈论组件对象模型(COM),多年以来,它一直是大多数Windows API的基石.同时,它也继续作为Windows运行时的基石.COM无可争辩的依附于C++的原始设计,借鉴了许多来自C++的二进制和语义约定,但是它从来都不够优雅.C++的部分内容被认为可移植性不够,如dynamic_cast,必须避免使用它,以采用可移植的解决方案,这使得C++的开发实现更具挑战性.近些年已经为C++开发者提供了许多解决方案,让COM变得更加可移植.C++/CX 语言拓展,可能是Visual C++团队到目前为止最具野心的.具有讽刺意味的是,这些提升标准C++支持的努力,已经将C++/CX弃之不顾了,也让语言拓展变得冗余.
为了证明这点,我会展示给你如何完整的用现代C++实现IUnknown和IInspectable接口.关于这两个接口没有什么现代的或吸引力的东西.IUnknown继续成为卓越API,如DirectX,的集中抽象.这些接口--IInspectable继承自IUnknown--位于Windows运行时的中心.我将展示给你如何不用任何语言拓展来实现它们,接口表或其它宏--只需要包含大量类型信息的高效和优雅的C++,就可以让编译器和开发者拥有,关于如何创建所需的,优异的人机对话.
主要的问题是, 如何列出 COM 或 Windows Runtime 类需要实现的接口, 而且要方便开发者使用, 和编译器访问. 比如, 列出所有可用类型, 以便编译器查询, 甚至枚举出相应的接口. 要是能实现这样的功能, 也许就能让编译器生成 IUnknown QueryInterface 甚至 IInspectable GetIids 方法的代码. 这两个方法才是问题的关键. 按照传统的观念, 唯一的解决办法涉及到语言扩展(language extensions), 万恶的宏定义, 以及一堆难以维护的代码.
两种方法的实现, 都用到类需要实现的接口. 可变参数模板( variadic template)是首选:
template <typename ... Interfaces> class __declspec(novtable) Implements : public Interfaces ... { };
__declspec(novtable)拓展属性可以防止构造函数和析构函数初始化抽象类的vfptr,这通常意味着减少大量的代码.实现类模板包括一个模板参数包,这使它成为一个可变模板.一个参数包即一个模板参数接受任意数目的模板参数变量.但是在这种情况下,我描述的模板参数将只会在编译时进行查询.接口将不会出现在函数的参数列表之中.
这些参数的一个使用已经显而易见.参数包拓展后成为公共基础类的参数列表.当然,我仍然有责任到最后实现这些虚函数,但是此刻我会描述一个实现任意数目接口的一个具体类:
class Hen : public Implements<IHen, IHen2> { };
因为参数包拓展为指定基础类的列表,所有它等同于下面我可能会写出的代码:
class Hen : public IHen, public IHen2 { };
用这种方式结构化实现类模板的美妙之处在于,我现在可以,在实现类模板中,写入各种样版实现代码,而Hen类的开发者则可以使用这种不唐突的抽象,同时大量忽略隐含的细节.
到目前为止,一切都很好.现在,我将考虑IUnknown的实现.我应该可以在实现类模板中完整的实现它,并提供编译器现在所拥有的类型信息.IUnknown提供了对于COM类非常重要的两种工具,就像氧气和水对于人类一样.第一个可能简单些的是引用计数,这也是COM对象跟踪它们生命周期的方式.COM规定一种侵入式的引用计数,它借助于每个对象,统计多少个外部引用存在,来负责管理自己的生命周期.这与智能指针,如C++ 11的shared_ptr类,的引用计数恰恰相反,智能指针对象并不知道它的共享关系.你可能会争论这两种方式的优缺点.但是,实际上COM的方法通常更高效,这也是COM的工作方式,你必须处理它.如果没有其它的,你很可能会同意这点,在shared_ptr里面包装一个COM接口会是一件极不友好的事情!
我将以只有运行时的开销作为开始,它是通过实现类模板介绍的:
protected: unsigned long m_references = 1; Implements() noexcept = default; virtual ~Implements() noexcept {}
默认构造函数并不是真正的开销所在,它只是简单的确保最终的构造函数--它将初始化引用计数--为protected而不是public的.引用计数和虚构造函数都是protected的.让派生类访问引用计数,是为了允许更复杂的类组合.大多数类可以简单的忽略它,但是需要注意的是,我正初始化引用计数为1.这和通常建议初始化引用计数为0,形成鲜明的对比,因为此时并没有处理引用.这个方式在ATL中非常流行,明显受到Don Box的COM本质论的影响,但是这是非常有问题的,ATL的源代码的研究可以作为佐证.开始于这个假设,即引用的所有权将会立即由调用者获得,或者依附于一个提供更少错误构造处理的智能指针.
虚析构函数提供了很大的便利性,它允许实现类模板实现引用计数,而不是强制实现类本身来提供实现.另一个选项,是使用奇特的递归模板模式(Curiously Recurring Template Pattern)来避免使用虚函数.通常我会选择这个方法,但是它会稍微增加抽象的复杂性,同时,因为COM类本身有一个vtable,所以这里也没有什么理由去避免使用虚函数.有了这些基本类型之后,在实现类模板中实现AddRef和Release将会变得非常简单.首先,AddRef方法可以简单的使用InterlockedIncrement来增加引用计数:
virtual unsigned long __stdcall AddRef() noexcept override { return InterlockedIncrement(&m_references); }
这不言自明.不要想出某些复杂的方法,通过使用C++的加减操作符来有条件的替换InterlockedIncrement和InterlockedDecrement函数.ATL通过极大的增加复杂性去做这个尝试.如果你考虑效率,宁可为避免调用AddRef和Release产生谬误而多花心思.同样的,现代C++增加了对move语义的支持,以及增加转移引用所有权的能力.现在,Release方法只是略显复杂:
virtual unsigned long __stdcall Release() noexcept override { unsigned long const remaining = InterlockedDecrement(&m_references); if (0 == remaining) { delete this; } return remaining; }
引用计数减少后,结果被赋值给临时变量.这很重要,因为结果需要返回.但是如果对象销毁了,引用此对象的成员变量就是非法的了.假定没有其它未处理的引用,这个对象就通过前面说到的虚析构函数删除了.这就是引用计数的结论,实现类Hen仍然和之前的一样简单:
class Hen : public Implements<IHen, IHen2> { };
现在,到了想象一下QueryInterface的奇妙世界的时间了。实现IUnknown方法是一个很重要的实践。在我的Pluralsight课程中,我广泛的实现了它。你可以在Don Box编写的<<COM本质论>>(Addison-Wesley Professional,1998)一书中,阅读关于实现你自己的IUnknown的奇妙的和不可思议的方法。需要注意的是,虽然这是一本关于COM的优秀书籍,但是它是基于C++98的,并没有呈现出任何现代C++的特征。为了节省时间,我假定你已经熟悉了QueryInterface的实现过程,并集中于如何用现代C++实现它。下面是虚函数本身:
virtual HRESULT __stdcall QueryInterface( GUID const & id, void ** object) noexcept override { }
给定一个GUID用来标识一个特别的接口之后,QueryInterface应该来决定一个对象是否实现了需要的接口。如果实现了,它必须减少这个对象的引用计数,同时通过外部参数来返回所需的接口指针。如果没有实现,它必须返回一个空指针。因此,我将以一个粗略的轮廓来作为开始:
*object = // Find interface somehow if (nullptr == *object) { return E_NOINTERFACE; } static_cast<::IUnknown *>(*object)->AddRef(); return S_OK;
QueryInterface首先会尝试设法查找所需的接口。如果接口受不支持,则返回E_NOINTERFACE错误码。请注意,我是如何按照要求处理接口指针不支持的情况。你应该把QueryInterface接口看作是二元的操作。它要么成功找到所需的接口,要么查找失败。不要尝试发挥创造性,只需要依据条件响应即可。尽管COM规范有一些限制项,但是大多数消费者都会简单的假定接口不受支持,而不管你会返回何种错误码。在你的实现中的任何错误,都毫无疑问的会导致你陷入调试的深渊。QueryInterface是非常基础的,不能胡乱对待。最后,AddRef由接口指针再次调用,用来支持某种极少的而又允许的类组合场景。这些不受实现类模板的显式支持,但是我情愿在这里做一个表率。重要的是,记住引用计数操作是面向接口的,而不是面向对象的。你不能 简单的,在属于一个对象的任意接口上面,调用AddRef或者Release。你必须依赖COM规则来管理对象,否则你会冒险引入以不可思议的方式崩溃的非法代码。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务