完。当我笼统地提及编译期递归时,重要的是要注意这个函数模板,以及其他类似的实现类模板的例子,在技术上递归,而不是在编译期。每个函数模板的实例调用不同的函数模板的实例。例如,FindInterface 调用 FindInterface,?FindInterface调用?FindInterface<>。为了让它递归,?FindInterface不需要调用FindInterface。
尽管如此,还是要记住,这样的“递归”发生在编译时,它就像你自己手写的一条条if语句。但是,现在,我遇到麻烦了。这个序列如何终止呢?当然是当模板参数列表为空的时候。这个问题在于C++已经定义了空模板参数列表的含义:
?
这几乎是正确的,但是编译器会提示你,函数模板在这个特化中无法使用。同时,如果我不提供终止函数,当参数包为空的时候,编译器将无法编译最终的调用。这不是函数重载的情况,因为参数列表依旧是相同的。幸运的是,解决方案非常简单。我可以通过提供一个无名的默认参数,来避免终止函数看起来像一个特化:
?
编译器乐于此,同时,如果请求一个不支持的接口,终止函数会简单的返回一个空指针,同时虚函数QueryInterface将返回E_NOINTERFACE错误码。对IUnknown而言,这考虑得很周到。如果你所关心的是经典的COM,你可以安全的停在那里,因为那就是你所需要的所有内容。关于这点,可以反复的操作,编译器将优化QueryInterface的实现,其间使用各种各样的“递归的”函数调用和常量表达式,代码至少和你手工写的一样好。对于IInspectable而言,同样的方式也可以实现。
对于Windows 运行时类,实现IInspectable会增加额外的复杂度。这个接口并非是和IUnknown一样的基础性接口,它提供了一些不确定的工具的集合,而IUnknown则提供了绝对基础的函数。关于此,我会为以后的文章留下一个讨论,并聚焦于支持任意Windows运行时类的高效和现代的C++实现。首先,我会避开GetRuntimeClassName和GetTrustLevel虚函数。实现这两个方法相对而言微不足道,同时由于极少使用,所以它们的实现可以简单搪塞一下。GetRunTimeClassName方法,需要返回这个对象所代表的运行时类的完整名字的字符串。我将把这留给类自身去完成,决定是否去这样做。实现类模板可以简单地返回E_NOTIMPL,用来表明此方法并未实现:
同样的,GetTrustLevel 方法也会简单返回枚举类型的常量:
需要注意的是,我并没有显示的标记这些IInspectable方法为虚函数。避免声明为虚函数,是为了让编译器剔除这些函数,COM类无需真正的实现任何 IInspectable 接口。现在,我将注意力转移到IInspectable GetIids 方法。这比 QueryInterface 更容易产生错误。尽管它的实现不是那么严格,但是一个高效的编译器生成的实现也是可取的。GetIids 返回一个动态分配的 GUID 数组。每个 GUID 代表一个对象要实现的接口。起初你可能会认为,这只是对象通过 QueryInterface 所支持的一个简单的声明,但是那只在表面上看是正确的。GetIids 方法可能会保留一些接口而不公开。不管怎样,我会以基本定义作为开始:
第一个参数指向调用者提供的变量,其中 GetIids 方法必须设置它为结果数组中的接口数目。第二个参数指向一个 GUID 数组,同时也表示着这里的实现是如何将动态分配的数组返回给调用者的。此处,我清除了这两者参数,只是为了安全起见。现在我??要决定这个类实现多少个接口。我想说的是,使用 sizeof 操作符,它可以确定这个参数包的大小,如下:
这相当方便,同时,编译器也会报告参数包拓展后要展现的模板参数的数目。这也是一个有效的常量表达式,它会在编译时产生一个值。我之前略为提及的,这无法实现的原因是,GetIids的实现会保留一些他们不愿和其他人共享的接口,这相当普遍。这些接口被称为隐含接口。任何人都可以通过QueryInterface来查询它们,但是GetIids不会告知这些接口是可用的。这样,我需要为排除了隐含接口的可变sizeof操作符,提供一个编译时的替代品。同时,我需要提供某种方式来声明和标识这些隐含接口。我以后者作为开始。对于组件开发人员,我希望让实现类变得尽可能简单,这样就需要一个不太引人注意的技巧。我简单的提供一个Cloaked类模板,用来“修饰”任意的隐含接口:
然后,我决定在类Hen上实现一个特别的"IHenNative"接口,并非所有的使用者都知道它的存在:
由于Cloaked类模板继承自它的模板参数,所以已存的QueryInterface实现可以继续无缝工作。我已经增加了一点额外的编译时的类型信息,现在我可以查询它们。由此,我会定义一个IsCloaked类型,这样,我就可以很容易的查询任意接口以判断它是否被隐藏:
现在,我可以使用一个递归的可变函数模板,来计算未隐藏的接口数目:
当然,我需要一个终止函数,可以简单的返回0:
使用现代C++在编译时进行算术计算的强大能力令人目瞪口呆,同时也简单的令人惊叹。现在我通过请求数量来继续完善GetIids的实现:
一个不太圆满的地方是,编译器对常量表达式的支持还不是很成熟。尽管,这毫无疑问是一个常量表达式,但是编译器却不会如此看待constexpr成员函数。理想情况下,我可以标识CountInterfaces函数模板为constexpr,同时结果表达式也将是一个常量表达式,但是,编译器不会这认为。另一方面,毋庸置疑,编译器会优化这段代码。现在,不管是什么原因,如果CountInterfaces没有找到隐含接口,GetIids会简单的返回成功,因为结果数组会为空:
同样的,这也是一个有效的常量表达式,编译器会无需任何条件的生成代码。换句话说,如果没有未隐藏的接口,剩下的代码会简单的从实现中删除。否则,代码的实现,会强制要求使用传统的COM分配器,分配一个合理大小的GUID数组:
当然,这可能失败,在这种情况下,我简单的返回合适的HRESULT值:
在这点上,GetIids准备好一个数组用来存放GUID。就像你所期待的那样,我需要最后一次枚举接口,然后拷贝每个未隐藏的接口的GUID到这个数组之中。我将使用一组函数模板,就像我之前使用的那样:
这个可变模板(第二个函数)可以简单的使用IsCloaked类型来决定,在增加指针之前,是否拷贝由First模板参数标识的接口的GUID。使用这种方式,可以遍历数组,而无需记录它包含多少个元素,或者它将写入数组的哪个位置。我也禁止了关于常量表达式的警告:
如你所见,在最后“递归”调用CopyInterfaces使用了可能增加的指针的值。我几乎完成了(整个实现过程)。在返回给调用者之前,GetIids的实现可以通过调用CopyInterfaces来填充数组:
对于Hen类来说,编译器在它上面的所有操作都是透明的(它完全不知道这些操作):
这就是一个好的库所应该有的样子。Visual C++ 2015编译器提供了Windows平台下面对于标准C++的完美支持。它允许C++开发者创建优雅而高效的库。这同样支持使用标准C++开发Windows运行时组件,以及完全