浅谈Visual C++ 2015引入更新的C++ 特性

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规则来管理对象,否则你会冒险引入以不可思议的方式崩溃的非法代码。

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

    virtual HRESULT __stdcall QueryInterface(  
      GUID const & id, void ** object) noexcept override  
    {  
      *object = BaseQueryInterface<Interfaces ...>(id);

BaseQueryInterface本质上是IUnknown QueryInterface的现代C++版本。它直接返回接口指针而不是HRESULT类型。空指针则表明失败的情况。它接受单一函数参数,GUID标识着要查找的接口。更重要的是,我拓展了类模板参数包为完整模式,这样,BaseQueryInterface函数就可以开始枚举接口的处理过程。 起初你可能会认为,由于BaseQueryInterface是实现类模板的成员函数,所以它可以简单直接的访问接口的链表,但是我需要准许这个函数剥离链表中的第一个接口, 建设网站就像下面这样:

    template <typename First, typename ... Rest>  
    void * BaseQueryInterface(GUID const & id) noexcept  
    {  
    }

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

    if (id == __uuidof(First) || id == __uuidof(::IUnknown))  
    {  
      return static_cast<First *>(this);  
    }

假设有一个匹配的,我直接准确无误的返回了第一个接口的指针。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>。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值