您好、欢迎来到现金彩票网!
当前位置:神州彩票app下载 > 公理语义 >

Visual C++ 2015 将现代 C++ 引入 Windows API

发布时间:2019-06-27 14:03 来源:未知 编辑:admin

  Visual C++ 2015 是 C++ 团队付出巨大努力将现代 C++ 引入 Windows 平台的巅峰之作。在以前的几个版本中,Visual C++ 逐渐增加了相当丰富的现代 C++ 语言和库功能,这些一起组成了绝对令人惊叹的环境,可在其中构建通用的 Windows 应用和组件。Visual C++ 2015 的构建基于早期版本中所引入的显著进展,提供成熟的编译器来支持大部分 C++11 以及一个 C++ 2015 子集。您可能会对完整性水平方面有所议论,但我认为,恰当地说,此编译器支持最重要的语言功能,使现代 C++ 开创了 Windows 库开发的新时代。而且,这确实是关键。只要编译器支持简练高效的库开发,开发人员就可以继续构建强大的应用和组件。

  我将完整地为您介绍一些传统复杂代码开发(由于 Visual C++ 编译器的成熟,现在确实很乐于编写这些代码),而不是只给您一个无聊的新功能列表或带您快速浏览这些功能。我将向您介绍 Windows 的本质以及几乎每个重要的当前和未来 API 的核心所在。

  有点讽刺的是,对于 COM 而言,C++ 终于足够现代了。没错,我说的正是组件对象模型,它多年来一直就是大部分 Windows API 的基础,并且继续作为 Windows 运行时的基础。虽然不可否认 COM 与 C++ 在其原始设计方面相关联,并借鉴了 C++ 的二进制和语义约定方面的内容,但 COM 始终不是很完善。部分 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 运行时类打算实现的接口列表,并且要以方便开发人员且编译器可以访问的方式执行此操作。尤其是,我需要提供此类型列表,以便编译器可以询问,甚至枚举这些接口。如果我可以实现上述操作,或许就能够让编译器生成 IUnknown QueryInterface 方法的代码,还可以选择生成 IInspectable GetIids 方法的代码。正是这两种方法构成了最大的挑战。一直以来,唯一的解决方案都涉及语言扩展、可怕的宏或者许多难以维护的代码。

  这两种方法的实现都需要类打算实现的接口列表。描述这种类型列表的自然而然的选择就是可变参数模板:

  novtable __declspec 扩展属性防止任何构造函数和析构函数在这种抽象类中初始化 vfptr,这通常意味着代码大小的显著降低。此 Implements 类模板包括一个模板参数包,从而使其成为可变参数模板。参数包是接受任意数量的模板实参的模板形参。其奥妙在于,参数包通常用于使函数接受任意数量的参数,但在这种情况下,我描述的是仅在编译时被询问参数的模板。这些接口绝不会在函数参数列表中显示。

  使用这些参数的用户已经清楚地了解这一点。参数包扩展至形成公共基类列表。当然,我仍负责实际实现这些虚函数,但在这一点上,我可以描述实现任意数量接口的具体类:

  以这种方法构建 Implements 类模板的优点在于:现在我可以将各种样板代码的实现插入 Implements 类模板,而 Hen 类的开发人员可以使用此不显眼的抽象,并在很大程度上忽略这一抽象背后的魔力。

  到目前为止一切顺利。现在,我会考虑 IUnknown 本身的实现。根据编译器现在可以处理的信息类型,我应该能够在 Implements 类模板中将其完全实现。IUnknown 提供两个对于 COM 类来说必不可少的功能,就像氧气和水之于人类。第一个功能是引用计数,或许它也是两个功能中较简单的功能,而且此功能是 COM 对象跟踪其生存期的方式。COM 规定侵入式引用计数的形式,以便每个对象根据其对存在的未完成引用数量的了解,来负责管理其自身的生存期。这与 C++11 shared_ptr 类模板等引用计数智能指针相反,在该类引用计数智能指针中对象并不知道其共享的所有权。您可能会争论这两种方法的利弊,但在实践中,COM 方法通常更为高效,这就是 COM 工作的方式,因此您必须解决这个问题。撇开其他不谈,您可能会认为在 shared_ptr 中封装 COM 接口是一个可怕的想法!

  默认的构造函数并不是真的在自身中开销,它只确保生成的构造函数(将初始化引用计数)受到保护,而不是公开。引用计数和虚拟析构函数都会受到保护。使引用计数可用于派生类,以便进行更复杂的类复合。大多数类可以直接忽略这一点,但请注意,我正在将引用计数初始化为一。由于尚未分发任何引用,这有悖于建议引用计数最初应为零的常识。这种方法由 ATL 推广,且无疑受到 Don Box 的《Essential COM》(COM 本质论)的影响,但这一方法很有问题,对 ATL 源代码的研究可以很好地证明这一点。首先假设引用的所有权将立即由调用方假定或附加到智能指针,这会提供不容易出错的构造过程。

  虚拟析构函数可以带来极大的便利,因为它允许 Implements 类模板实现引用计数,而不是强制具体类本身提供此实现。另一个选择是使用奇特的重复执行模板模式来避免虚拟函数。通常,我更喜欢这种方法,但它使抽象略微复杂化。由于 COM 类在其本质上具有 vtable,因此不必在此处避免虚拟函数。准备好这些原型之后,在 Implements 类模板中实现 AddRef 和 Release 就会变得非常简单。首先,AddRef 方法只需使用 InterlockedIncrement 内部函数来提升引用计数:

  大部分都很好理解。不要尝试提出一些复杂的方案,由此,您可能需要用 C++ 增量和递减运算符来有条件地替代 InterlockedIncrement 和 InterlockedDecrement 内部函数。ATL 尝试这样做是以复杂性为巨大代价的。如果您关注的是效率,那就不要费劲来避免对 AddRef 和 Release 的虚假调用了。同样,现代 C++ 通过支持移动语义和移动引用所有权这一功能来解决这一问题,而无需增加引用。现在,Release 方法只是稍微更为复杂:

  引用计数递减,并将结果分配到本地变量。这一点非常重要,因为该结果应该被返回,但如果要销毁该对象,则它将非法引用成员变量。假设没有未完成的引用,则仅通过调用前面所提及的虚拟析构函数即可删除该对象。这将得出引用计数,并且具体的 Hen 类仍然和之前一样简单:

  现在是时候来探讨神奇的 QueryInterface 世界了。实现此 IUnknown 方法是一个重要的运用。在我的 Pluralsight 课程中,我进行了详细介绍,您也可以阅读 Don Box 的《Essential COM》(COM 本质论)(Addison-Wesley Professional, 1998) 一书,了解许多奇妙的方式,帮助您动手编写自己的实现。请注意,虽然这是一本有关 COM 的优秀书籍,但它基于 C++98,完全不能表现现代 C++。为了节省空间和时间,我假设您对 QueryInterface 实现有一定的了解,并转而关注如何使用现代 C++ 来实现它。下面是虚拟方法本身:

  鉴于 GUID 标识一个特定接口,QueryInterface 应确定该对象是否实现所需的接口。如果实现,则它必须增加该对象的引用计数,然后通过输出参数返回所需的接口指针。如果没有实现,则它必须返回 nulll 指针。因此,我先做个大体概述:

  首先 QueryInterface 尝试以某种方式查找所需的接口。如果不支持此接口,则返回必要的 E_NO­INTERFACE 错误代码。请注意我如何考虑失败时所生成接口指针的清理要求。您应该把 QueryInterface 看作是二进制操作。它要么成功地找到所需接口,要么没有。不要尝试在这里发挥创造性,只有有条件地响应更具优势。虽然 COM 规范允许一些有限的选项,但大多数消费者会简单地认为不支持该接口,无论可能返回何种故障代码。实现中的任何错误无疑都会导致无穷无尽的痛苦调试。QueryInterface 是基础中的基础,不能出错。最后,再次通过生成的接口指针调用 AddRef,以支持一些罕见但允许的类复合情况。Implements 类模板不显式支持这些情况,但我宁愿在这里树立个好榜样。要牢记的一点就是,引用计数操作是特定于接口的,而并不是特定于对象的。您不能简单地在属于某个对象的任意接口上调用 AddRef 或 Release。您必须遵守管理对象标识的 COM 规则,否则您就有可能引入以神秘方式造成中断的非法代码。

  那么,我要如何发现请求的 GUID 是否表示的是该类打算实现的接口?我从这里可以返回到 Implements 类模板通过其模板参数包收集的类型信息。请记住,我的目标是让编译器为我实现这一操作。我希望生成的代码就像我自己编写的代码一样高效,或者更为高效。因此,我将使用一组可变参数函数模板来执行此查询,这些函数模板本身就包含模板参数包。我将着手从 BaseQueryInterface 函数模板开始:

  BaseQueryInterface 本质上是 IUnknown QueryInterface 的现代 C++ 投影。它直接返回接口指针,而不是返回 HRESULT。用 null 指针明显表示失败。它接受单个函数参数,GUID 标识要查找的接口。更重要的是,我整体扩展了类模板的参数包,以便 BaseQueryInterface 函数可以开始进行枚举接口的过程。起初,您可能认为这是因为 BaseQueryInterface 是 Implements 类模板的成员,它才能够直接访问此接口列表,但我需要允许此函数剔除列表中的第一个接口,如下所示:

  这样,BaseQueryInterface 可以识别第一个接口,然后保留剩余接口以便进行后续搜索。您会发现,COM 具有许多具体的规则来支持 QueryInterface 必须实现或至少遵守的对象标识。尤其是,对 IUnknown 的请求必须始终返回完全相同的指针,以便客户端确定两个接口指针是否引用了相同的对象。如此,BaseQueryInterface 函数成为实现某些公理的好去处。因此,我先来比较请求的 GUID 和代表该类打算实现的第一个接口的第一个模板参数。如果两者不匹配,我将检查是否正在请求 IUnknown:

  假设其中有一个匹配,我只返回第一个接口的明确接口指针。static_cast 确保编译器不会在基于 IUnknown 的多个接口歧义方面产生问题。该转换只调整指针来查找类 vtable 中的正确位置,由于所有接口 vtable 都从 IUnknown 的三种方法开始,因此这完全有效。

  虽然在这里,我也可以添加 IInspectable 查询的可选支持。IInspectable 是一个相当奇怪的“怪兽”。在某种意义上,IInspectable 是 Windows 运行时的 IUnknown,因为投射到 C# 和 JavaScript 等语言中的每个 Windows 运行时接口必须直接从 IInspectable 派生,而不是仅从 IUnknown 派生。这是一个令人遗憾的现实,以便适应通用语言运行时实现对象和接口的方式,这一方式与 C++ 的工作方式以及传统定义 COM 的方式相反。当涉及到对象组合时,这也会产生一些相当令人遗憾的性能影响,但这是一个范围广泛的主题,我将在后续文章中进行介绍。就 QueryInterface 而言,我只需确保这是 Windows 运行时类的实现,而不仅仅是经典的 COM 类的实现时,可以查询 IInspectable。虽然有关 IUnknown 的显式 COM 规则不适用于 IInspectable,但在这里我可以用几乎相同的方法对待后者。但这面临两大挑战。首先,我需要发现是否有任何实现的接口从 IInspectable 派生。其次,我需要这类接口的类型,以便返回适当调整过的接口指针,并且不会引起歧义。如果我可以假设列表中的第一个接口始终基于 IInspectable,则只需更新 BaseQueryInterface,如下所示:

  请注意,我正在使用 C++11 is_base_of 类型特性来确定第一个模板参数是否为 IInspectable 派生的接口。这样可以确保当您在没有 Windows 运行时支持的情况下实现经典的 COM 类时,编译器将不执行后续比较。这样,我可以无缝地支持 Windows 运行时和经典的 COM 类,而对组件开发人员来说不会产生任何额外的句法复杂性,同时也没有任何不必要的运行时开销。但在您先列出非 IInspectable 接口的情况下,这会为一个非常难以发现的 bug 埋下隐患。我需要做的就是用能扫描整个接口列表的东西来替换 is_base_of:

  IsInspectable 仍依赖于 is_base_of 类型特性,现在将它应用于每个接口,直到找到一个匹配的接口。如果找不到基于 IInspectable 的接口,则会找到结束函数:

  稍后我再介绍奇妙的无名默认参数。假设 IsInspectable 返回 true,我需要查找第一个基于 IInspectable 的接口:

  我可以再次依赖 is_base_of 类型特性,但在找到匹配的情况下,这次返回一个实际的接口指针:

  请注意,我正在调用的 FindInterface 函数模板,其方式与我最初调用的 BaseQueryInterface 非常相似。在这种情况下,我将这些接口的其余部分传递给它。具体来说,我将扩展参数包,这样它就能再次识别列表其余部分中的第一个接口。但这面临一个问题。由于模板参数包无法扩展为函数参数,最终可能出现令人烦恼的情况,即语言无法表达出我真正想要表达的内容。稍后我会详细进行说明。“递归”FindInterface 可变参数模板正是您所期望的:

  该模板将它的第一个模板参数与其余部分区分开来,如果存在匹配,则返回调整的接口指针。否则,该模板会调用其本身,直到接口列表耗尽。虽然我将这统统称为编译时递归,但值得注意的是,即使在编译时,此函数模板以及 Implements 类模板中其他类似的示例都不是技术上的递归。每个函数模板的实例化都调用不同的函数模板实例化。例如,FindInterfaceIHen, IHen2 调用 FindInterfaceIHen2,这会调用 FindInterface。为了使其具有递归性,FindInterfaceIHen, IHen2 可能需要调用 FindInterfaceIHen, IHen2,但实际上它并不会这样做。

  尽管如此,请记住,此“递归”在编译时发生,这就像是您手动编写所有这些 if 语句,一个接一个。但现在我遇到了阻碍。这一序列如何终止?当然,是在模板参数列表为空的时候。问题是 C++ 已经定义了空的模板参数列表意味着什么:

  这几乎是正确的,但编译器会告诉您函数模板不适用于此特殊化。但是,如果我不提供此结束函数,在参数包为空时,编译器将无法编译最后一次调用。这不是函数重载的情况,因为参数列表是相同的。幸运的是,解决方案非常简单。我可以为结束函数提供一个无名默认参数,来避免其看上去像特殊化:

  该编译器令人满意,如果请求不受支持的接口,此结束函数只需返回一个 null 指针,虚拟 QueryInterface 方法将返回 E_NOINTERFACE 错误代码。并且这处理了 IUnknown。如果您所关心的只是经典 COM,到这儿您就可以放心地停下来了,因为这些就是您所需要的一切。值得重申的一点就是,编译器将通过其各种“递归”函数调用和常量表达式来优化 QueryInterface 实现,这样一来,该代码就跟您手动编写的一样好。IInspectable 可以达到同样的效果。

  对于 Windows 运行时类,实现 IInspectable 会增加复杂性。该接口完全不如 IUnknown 般基础,相比 IUnknown 所提供的绝对必要的函数,该接口提供可疑的工具集合。不过,我会在以后的文章中讨论此问题,本文关注的是支持任何 Windows 运行时类的高效且现代的 C++ 实现。首先,我将挑出 GetRuntimeClassName 和 GetTrustLevel 虚拟函数。这两种方法都比较容易实现,并且也很少使用,因此它们的实现可以基本省略。GetRuntimeClassName 方法应返回带有该对象所表示的运行时类完整名称的 Windows 运行时字符串。在该类决定这样做的情况下,我将决定权留给类自身来实现。Implements 类模板可以返回 E_NOTIMPL 来指明尚未实现此方法:

  请注意,我并没有将这些 IInspectable 方法显式标记为虚拟函数。在 COM 类实际上并未实现任何 IInspectable 接口的情况下,避免虚拟声明允许编译器去掉这些方法。现在,我将注意力转移到 IInspectable GetIids 方法。这甚至比 QueryInterface 更容易出错。尽管 IInspectable GetIids 方法的实现几乎不怎么关键,但需要编译器生成的高效实现。GetIids 返回 GUID 动态分配的数组。每个 GUID 代表一个对象想要实现的接口。起初,您可能认为这只是该对象通过 QueryInterface 所支持内容的声明,但只有从表面上看,这一理解才正确。GetIids 方法可能会决定从发布中保留一些接口。不管怎样,我将从其基本的定义着手:

  第一个参数指向调用方提供的变量,GetIids 方法必须将此变量设置为生成数组中接口的数量。第二个参数指向一个 GUID 的数组,该参数是实现将动态分配数组传送回调用方的方式。在这里,为了安全起见,我首先清除了这两个参数。我现在需要确定该类要实现的接口数量。我想说,只需使用能提供参数包大小的 sizeof 运算符,如下所示:

  这非常便捷,该编译器令人满意地报告扩展此参数包时会显示模板参数的数量。这也是有效的常量表达式,生成编译时已知的值。如前所述,不执行此操作的原因是因为以下情况相当常见,即 GetIids 实现会保留一些不想与每个人共享的接口。这些接口称为掩蔽的接口。任何人都可以通过 QueryInterface 查询这些接口,但是 GetIids 不会告诉您这些接口可用。因此,我需要为排除掩蔽接口的可变参数 sizeof 运算符提供编译时替代,并且需要提供某种方式来声明和标识这类掩蔽的接口。我将从后者着手。我希望使其尽可能简化组件开发人员实现类的工作,因此一种相对不显眼的机制在这里很适用。我只需提供 Cloaked 类模板来“修饰”任何掩蔽的接口:

  然后,我可以决定在所有消费者都不知道的具体 Hen 类上实现特殊的“IHenNative”接口:

  因为 Cloaked 类模板从其模板参数中派生而来,因此现有的 QueryInterface 实现可以继续无缝地工作。我只添加了一点额外的类型信息,现在就可以再次在编译时对其进行查询。为此,我将定义 IsCloaked 类型特性,这样我可以轻松地查询任何接口以确定其是否被掩蔽:

  能在编译时使用现代的 C++ 执行这种算数计算的这一功能极为强大,又惊人地简单。现在,我可以通过请求该计数来继续填充 GetIids 实现:

  存在的一个小问题就是,编译器对常量表达式的支持还不是很成熟。虽然这无疑是一个常量表达式,但编译器不遵守 constexpr 成员函数。理想情况下,我可以将 CountInterfaces 函数模板标记为 constexpr,所生成的表达式将同样是一个常量表达式,但编译器却不这么认为。另一方面,毋庸置疑,编译器在优化此代码时不会有太大困难。现在,如果 CounInterfaces 因任何原因没有找到非掩蔽的接口,GetIids 只需返回成功信息,因为所生成的数组将为空:

  同样,这实际上是一个常量表达式,编译器将生成代码,而无需某种形式的条件。换言之,如果非掩蔽的接口不存在,则直接从实现中删除剩余代码。否则,该实现不得不使用传统的 COM 分配器来分配适当大小的 GUID 数组:

  此时,GetIids 拥有一个准备填充 GUID 的数组。正如您所料,我需要最后一次枚举这些接口,来将每个非掩蔽接口的 GUID 复制到此数组。我将像以前所做过的操作一样,使用一对函数模板:

  可变参数模板(第二个函数)只需使用 IsCloaked 类型特性即可确定在递增指针之前,是否要复制由其第一个模板参数所标识的接口 GUID。通过这种方式,在遍历该数组时,无需追踪它所包含的元素数量,以及它应在数组中进行编写的位置。我还禁止显示有关此常量表达式的警告:

  正如您所见,最终对 CopyInterfaces 的“递归”调用使用了潜在递增的指针值。快要大功告成了。然后 GetIids 实现在将 CopyInterfaces 返回到调用方之前,最后可以调用它来填充该数组:

  这是因为它应该适用于任何好的库。Visual C++ 2015 编译器为 Windows 平台上的标准 C++ 提供出色支持。它使 C++ 开发人员构建非常简练有效的库。它同时支持标准 C++ 中的 Windows 运行时组件的开发,以及来自于完全在标准 C++ 中编写的通用 Windows 应用的消耗。Implements 类模板只是适用于 Windows 运行时的现代 C++ 的一个示例(请参阅。

http://39-5963.net/gongliyuyi/392.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有