Visual C++ 2015引入更新的C++ 特性到Windows API

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来增加引用计数:

这不言自明.不要想出某些复杂的方法,通过使用C++的加减操作符来有条件的替换InterlockedIncrement和InterlockedDecrement函数.ATL通过极大的增加复杂性去做这个尝试.如果你考虑效率,宁可为避免调用AddRef和Release产生谬误而多花心思.同样的,现代C++增加了对move语义的支持,以及增加转移引用所有权的能力.现在,Release方法只是略显复杂:

现在,到了想象一下QueryInterface的奇妙世界的时间了。实现IUnknown方法是一个很重要的实践。在我的Pluralsight课程中,我广泛的实现了它。你可以在Don Box编写的<<COM本质论>>(Addison-Wesley Professional,1998)一书中,阅读关于实现你自己的IUnknown的奇妙的和不可思议的方法。需要注意的是,虽然这是一本关于COM的优秀书籍,但是它是基于C++98的,并没有呈现出任何现代C++的特征。为了节省时间,我假定你已经熟悉了QueryInterface的实现过程,并集中于如何用现代C++实现它。下面是虚函数本身:

QueryInterface首先会尝试设法查找所需的接口。如果接口受不支持,则返回E_NOINTERFACE错误码。请注意,我是如何按照要求处理接口指针不支持的情况。你应该把QueryInterface接口看作是二元的操作。它要么成功找到所需的接口,要么查找失败。不要尝试发挥创造性,只需要依据条件响应即可。尽管COM规范有一些限制项,但是大多数消费者都会简单的假定接口不受支持,而不管你会返回何种错误码。在你的实现中的任何错误,都毫无疑问的会导致你陷入调试的深渊。QueryInterface是非常基础的,不能胡乱对待。最后,AddRef由接口指针再次调用,用来支持某种极少的而又允许的类组合场景。这些不受实现类模板的显式支持,但是我情愿在这里做一个表率。重要的是,记住引用计数操作是面向接口的,而不是面向对象的。你不能 简单的,在属于一个对象的任意接口上面,调用AddRef或者Release。你必须依赖COM规则来管理对象,否则你会冒险引入以不可思议的方式崩溃的非法代码。

但是我如何得知,请求的GUID是否就代表着类想要实现的接口呢?我需要回到实现类模板收集的类型信息的地方,其中类型信息通过它的模板参数包来收集。请记住,我的目标是准许编译器为我实现它。我希望最终代码,和我手写的一样高效,甚至更好。我会通过可变函数模板集合来进行查询,函数模板自身包括模板参数包。我将以BaseQueryInterface函数模板作为开始:

按照这种方式,BaseQueryInterface函数能识别第一个接口,并且给接下来的搜索留有余地。看吧,COM有一定数量的特殊规则来支持QueryInterface 实现或至少接受对象识别。尤其是请求IUnknown,必须总是返回确切相同的指针,客户端才能确定两个接口的指针是否来自同一个对象。因此,BaseQueryInterface函数最棒的地方就是实现了这些假设。所以,可以从首个代表类想要实现的第一个接口的模板参数的GUID请求的对比开始。如果不匹配,我会检查IUnknown是否开始请求了:

假设有一个匹配的,我直接准确无误的返回了第一个接口的指针。static_cast 能确保编译器基于IUnknown的多种接口不会引起歧义。cast只是校准了指针,让类的vtable能找到正确的指针位置,因为所有vtable接口是以IUnknown的三个方法开始的,这非常符合逻辑。

而我不妨同样添加IInspectable查询的可选支持。IInspectable相当变态,在某种意义上,它是Windows运行时接口,因为每个Windows运行时预计编程语言(如 C# 和 JavaScript)必须直接来自IInspectable,而不仅仅只是IUnknown接口。相对于C++的工作方式和COM传统的定义方式,以适应公共语言运行库的方式实现对象和接口是不幸的事实。更不幸的是当对象组合的时候对性能的影响,我会在下文中讨论。至于QueryInterface,我只需确保IInspectable能被查询,它应该是一个Windows运行时类的实现,而不是一个简单的典型COM类。虽然关于IUnknown的明确的COM规则不适用于IInspectable,我可以简单的用相同的方式对待后者。但这两个挑战。首先,需要了解是否有任何IInspectable派生出来的接口实现。第二,需要了解接口的类型,这样就可以正确的返回一个没有歧义的调整过的接口指针。假定列表中的第一个接口都是基于IInspectable,那可以只更新BaseQueryInterface 如下

if (id == __uuidof(First) ||  


  id == __uuidof(::IUnknown) ||  


  (std::is_base_of<::IInspectable, First>::value &&  


  id == __uuidof(::IInspectable)))  


{  



  return static_cast<First *>(this);  



} 

注意,我用的是C++ 11中的is_base_of 的特性,来确定第一个模板参数是一个IInspectable的衍生接口。万一实现典型的COM类不支持Windows运行时,就能确保随后的对照是由编译器排除的。这样我可以无缝地支持Windows运行时和经典的COM类,即没有增加组件开发人员的语句复杂性,也没有任何不必要的运行时开销。但是,如果恰好遇列举出来得首位不是IInspectable接口,就会有不容易察觉的Bug的隐患。所需要做的就是,用某种方法替代is_base_of来扫描整个接口的列表:

template <typename First, typename ... Rest>  



constexpr bool IsInspectable() noexcept  



{  



  return std::is_base_of<::IInspectable, First>::value ||  



    IsInspectable<Rest ...>();  


} 

IsInspectable 也是基于is_base_of特性的,但是当前适用于匹配接口。如果没找到基于IInspectable 的接口则终止:

template <int = 0>  



constexpr bool IsInspectable() noexcept  



{  



  return false;  



} 

我会禁用掉稀奇古怪的默认参数。假定 IsInspectable 返回的是 true,我需要找到第一个IInspectable-based 接口:

template <int = 0>  



void * FindInspectable() noexcept  



{  



  return nullptr;  



}  



template <typename First, typename ... Rest>  




void * FindInspectable() noexcept  



{  



  // Find somehow  



} 

再次使用 is_base_of 特性,但这次要返回一个真实匹配的接口指针:

#pragma warning(push)  



#pragma warning(disable:4127) // conditional expression is constant  




if (std::is_base_of<::IInspectable, First>::value)  



{  



  return static_cast<First *>(this);  



}  



#pragma warning(pop)  




return FindInspectable<Rest ...>(); 

BaseQueryInterface 这时可以利用IsInspectable 和 FindInspectable 一起来支持查询 IInspectable:

if (IsInspectable<Interfaces ...>() &&   


  id == __uuidof(::IInspectable))  


{  



  return FindInspectable<Interfaces ...>();  



} 

然后指定具体的 Hen 类:

class Hen : public Implements<IHen, IHen2>  


{  


}; 

实现类的模板,可以确保编译器能生成更高效的代码,不管 IHen、Hen2 来自 IInspectable 还是 IIUnknown (或者其他接口)。现在,我可以最后实现 QueryInterface 的递归部分,以及任何追加的接口,例如上面例子中的 IHen2。BaseQueryInterface 是靠调用 FindInterface 函数模板结束的:

template <typename First, typename ... Rest>  



void * BaseQueryInterface(GUID const & id) noexcept  



{  



  if (id == __uuidof(First) || id == __uuidof(::IUnknown))  



  {  



    return static_cast<First *>(this);  



  }  



  if (IsInspectable<Interfaces ...>() &&   



    id == __uuidof(::IInspectable))  


  {  



    return FindInspectable<Interfaces ...>();  



  }  



  return FindInterface<Rest ...>(id);  



} 

注意,我调用这个FindInterface函数模板,大致等同于我原来调用的BaseQueryInterface,在这个例子中,我向它传递接口的其余部分。我特意再次扩大参数包,这样它可以在列表的其余部分识别第一接口。但会提示一个故障。由于模板参数包不是以函数实参来扩展的,这可能会变得棘手,编程语言写不出来我想要的。更多的时候,这种“递归的”FindInterface可变模板正是你想要的:

template <typename First, typename ... Rest>  



void * FindInterface(GUID const & id) noexcept  



{  



  if (id == __uuidof(First))  



  {  



    return static_cast<First *>(this);  



  }  



  return FindInterface<Rest ...>(id);  



} 

它会从模板参数的其余部分中分离,如果有匹配就返回调整过的接口指针。另外,它也会调用自己,直到list取完。当我笼统地提及编译期递归时,重要的是要注意这个函数模板,以及其他类似的实现类模板的例子,在技术上递归,而不是在编译期。每个函数模板的实例调用不同的函数模板的实例。例如,FindInterface<IHen, IHen2> 调用 FindInterface<IHen2>, FindInterface<IHen2>调用 FindInterface<>。为了让它递归, FindInterface<IHen, IHen2>不需要调用FindInterface<IHen, IHen2>。

尽管如此,还是要记住,这样的“递归”发生在编译时,它就像你自己手写的一条条if语句。但是,现在,我遇到麻烦了。这个序列如何终止呢?当然是当模板参数列表为空的时候。这个问题在于C++已经定义了空模板参数列表的含义:

template <>  



void * FindInterface(GUID const &) noexcept  



{  



  return nullptr;  



} 

这几乎是正确的,但是编译器会提示你,函数模板在这个特化中无法使用。同时,如果我不提供终止函数,当参数包为空的时候,编译器将无法编译最终的调用。这不是函数重载的情况,因为参数列表依旧是相同的。幸运的是,解决方案非常简单。我可以通过提供一个无名的默认参数,来避免终止函数看起来像一个特化:

template <int = 0>  



void * FindInterface(GUID const &) noexcept  



{  



  return nullptr;  



} 

编译器乐于此,同时,如果请求一个不支持的接口,终止函数会简单的返回一个空指针,同时虚函数QueryInterface将返回E_NOINTERFACE错误码。对IUnknown而言,这考虑得很周到。如果你所关心的是经典的COM,你可以安全的停在那里,因为那就是你所需要的所有内容。关于这点,可以反复的操作,编译器将优化QueryInterface的实现,其间使用各种各样的“递归的”函数调用和常量表达式,代码至少和你手工写的一样好。对于IInspectable而言,同样的方式也可以实现。

对于Windows 运行时类,实现IInspectable会增加额外的复杂度。这个接口并非是和IUnknown一样的基础性接口,它提供了一些不确定的工具的集合,而IUnknown则提供了绝对基础的函数。关于此,我会为以后的文章留下一个讨论,并聚焦于支持任意Windows运行时类的高效和现代的C++实现。首先,我会避开GetRuntimeClassName和GetTrustLevel虚函数。实现这两个方法相对而言微不足道,同时由于极少使用,所以它们的实现可以简单搪塞一下。GetRunTimeClassName方法,需要返回这个对象所代表的运行时类的完整名字的字符串。我将把这留给类自身去完成,决定是否去这样做。实现类模板可以简单地返回E_NOTIMPL,用来表明此方法并未实现:

HRESULT __stdcall GetRuntimeClassName(HSTRING * name) noexcept  


{  


  *name = nullptr;  



  return E_NOTIMPL;  



} 

同样的,GetTrustLevel 方法也会简单返回枚举类型的常量:

HRESULT __stdcall GetTrustLevel(TrustLevel * trustLevel) noexcept  


{  


  *trustLevel = BaseTrust;  



  return S_OK;  



} 

需要注意的是,我并没有显示的标记这些IInspectable方法为虚函数。避免声明为虚函数,是为了让编译器剔除这些函数,COM类无需真正的实现任何 IInspectable 接口。现在,我将注意力转移到IInspectable GetIids 方法。这比 QueryInterface 更容易产生错误。尽管它的实现不是那么严格,但是一个高效的编译器生成的实现也是可取的。GetIids 返回一个动态分配的 GUID 数组。每个 GUID 代表一个对象要实现的接口。起初你可能会认为,这只是对象通过 QueryInterface 所支持的一个简单的声明,但是那只在表面上看是正确的。GetIids 方法可能会保留一些接口而不公开。不管怎样,我会以基本定义作为开始:

HRESULT __stdcall GetIids(unsigned long * count,   


  GUID ** array) noexcept  


{  


  *count = 0;  


  *array = nullptr; 

第一个参数指向调用者提供的变量,其中 GetIids 方法必须设置它为结果数组中的接口数目。第二个参数指向一个 GUID 数组,同时也表示着这里的实现是如何将动态分配的数组返回给调用者的。此处,我清除了这两者参数,只是为了安全起见。现在我需要决定这个类实现多少个接口。我想说的是,使用 sizeof 操作符,它可以确定这个参数包的大小,如下:

unsigned const size = sizeof ... (Interfaces); 

这相当方便,同时,编译器也会报告参数包拓展后要展现的模板参数的数目。这也是一个有效的常量表达式,它会在编译时产生一个值。我之前略为提及的,这无法实现的原因是,GetIids的实现会保留一些他们不愿和其他人共享的接口,这相当普遍。这些接口被称为隐含接口。任何人都可以通过QueryInterface来查询它们,但是GetIids不会告知这些接口是可用的。这样,我需要为排除了隐含接口的可变sizeof操作符,提供一个编译时的替代品。同时,我需要提供某种方式来声明和标识这些隐含接口。我以后者作为开始。对于组件开发人员,我希望让实现类变得尽可能简单,这样就需要一个不太引人注意的技巧。我简单的提供一个Cloaked类模板,用来“修饰”任意的隐含接口:

template <typename Interface>  



struct Cloaked : Interface {}; 

然后,我决定在类Hen上实现一个特别的"IHenNative"接口,并非所有的使用者都知道它的存在:

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>  


{  


}; 

由于Cloaked类模板继承自它的模板参数,所以已存的QueryInterface实现可以继续无缝工作。我已经增加了一点额外的编译时的类型信息,现在我可以查询它们。由此,我会定义一个IsCloaked类型,这样,我就可以很容易的查询任意接口以判断它是否被隐藏:

template <typename Interface>  



struct IsCloaked : std::false_type {};  




template <typename Interface>  




struct IsCloaked<Cloaked<Interface>> : std::true_type {}; 

现在,我可以使用一个递归的可变函数模板,来计算未隐藏的接口数目:

template <typename First, typename ... Rest>  


constexpr unsigned CounInterfaces() noexcept  


{  



  return !IsCloaked<First>::value + CounInterfaces<Rest ...>();  



} 

当然,我需要一个终止函数,可以简单的返回0:

template <int = 0>  


constexpr unsigned CounInterfaces() noexcept  


{  



  return 0;  



} 

使用现代C++在编译时进行算术计算的强大能力令人目瞪口呆,同时也简单的令人惊叹。现在我通过请求数量来继续完善GetIids的实现:

unsigned const localCount = CounInterfaces<Interfaces ...>(); 

一个不太圆满的地方是,编译器对常量表达式的支持还不是很成熟。尽管,这毫无疑问是一个常量表达式,但是编译器却不会如此看待constexpr成员函数。理想情况下,我可以标识CountInterfaces函数模板为constexpr,同时结果表达式也将是一个常量表达式,但是,编译器不会这认为。另一方面,毋庸置疑,编译器会优化这段代码。现在,不管是什么原因,如果CountInterfaces没有找到隐含接口,GetIids会简单的返回成功,因为结果数组会为空:

if (0 == localCount)  


{  



  return S_OK;  



} 

同样的,这也是一个有效的常量表达式,编译器会无需任何条件的生成代码。换句话说,如果没有未隐藏的接口,剩下的代码会简单的从实现中删除。否则,代码的实现,会强制要求使用传统的COM分配器,分配一个合理大小的GUID数组:

GUID * localArray = static_cast<GUID *>(CoTaskMemAlloc(sizeof(GUID) * localCount)); 

当然,这可能失败,在这种情况下,我简单的返回合适的HRESULT值:

if (nullptr == localArray)  


{  



  return E_OUTOFMEMORY;  



} 

在这点上,GetIids准备好一个数组用来存放GUID。就像你所期待的那样,我需要最后一次枚举接口,然后拷贝每个未隐藏的接口的GUID到这个数组之中。我将使用一组函数模板,就像我之前使用的那样:

template <int = 0>  



void CopyInterfaces(GUID *) noexcept {}  




template <typename First, typename ... Rest>  




void CopyInterfaces(GUID * ids) noexcept  



{  


} 

这个可变模板(第二个函数)可以简单的使用IsCloaked类型来决定,在增加指针之前,是否拷贝由First模板参数标识的接口的GUID。使用这种方式,可以遍历数组,而无需记录它包含多少个元素,或者它将写入数组的哪个位置。我也禁止了关于常量表达式的警告:

#pragma warning(push)  



#pragma warning(disable:4127) // Conditional expression is constant  




if (!IsCloaked<First>::value)  



{  


  *ids++ = __uuidof(First);  


}  



#pragma warning(pop)  



CopyInterfaces<Rest ...>(ids); 

如你所见,在最后“递归”调用CopyInterfaces使用了可能增加的指针的值。我几乎完成了(整个实现过程)。在返回给调用者之前,GetIids的实现可以通过调用CopyInterfaces来填充数组:

CopyInterfaces<Interfaces ...>(localArray);  


  *count = localCount;  


  *array = localArray;  



  return S_OK;  



} 

对于Hen类来说,编译器在它上面的所有操作都是透明的(它完全不知道这些操作):

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>  


{  


}; 

这就是一个好的库所应该有的样子。Visual C++ 2015编译器提供了Windows平台下面对于标准C++的完美支持。它允许C++开发者创建优雅而高效的库。这同样支持使用标准C++开发Windows运行时组件,以及完全使用标准C++编写的通用的Windows应用程序。实现类模板仅仅只是现代C++针对Windows运行时的一个例子。(查看 moderncpp.com).

Kenny Kerr 是一位加拿大的开发者,同样也是Pluralsight的作者,以及微软的MVP。 它的博客是 kennykerr.ca。你可以关注他的Twitter账号twitter.com/kennykerr

多谢微软的技术专家James McNellis审阅这篇文章。

相关推荐