加载中

Visual C++ 2015 is the culmination of a huge effort by the C++ team to bring modern C++ to the Windows platform. Over the last few releases, Visual C++ has steadily gained a rich selection of modern C++ language and library features that together make for an absolutely amazing environment in which to build universal Windows apps and components. Visual C++ 2015 builds on the remarkable progress introduced in those earlier releases and provides a mature compiler that supports much of C++11 and a subset of C++ 2015. You might argue about the level of completeness, but I think it’s fair to say that the compiler supports the most important language features, enabling modern C++ to usher in a new era of library development for Windows. And that really is the key. As long as the compiler supports the development of efficient and elegant libraries, developers can get on with building great apps and components.

Rather than giving you a boring list of new features or pro­viding a high-level whirlwind tour of capabilities, I’ll instead walk you through the development of some traditionally complex code that is now, frankly, quite enjoyable to write, thanks to the maturity of the Visual C++ compiler. I’m going to show you something that’s intrinsic to Windows and at the heart of practically every significant current and future 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中实际上都是很重要的本质。

It’s somewhat ironic that C++ is finally modern enough for COM. Yes, I’m talking about the Component Object Model that has been the foundation for much of the Windows API for years, and continues as the foundation for the Windows Runtime. While COM is undeniably tied to C++ in terms of its original design, borrowing much from C++ in terms of binary and semantic conventions, it has never been entirely elegant. Parts of C++ that were not deemed portable enough, such as dynamic_cast, had to be eschewed in favor of portable solutions that made C++ implementations more challenging to develop. Many solutions have been provided over the years to make COM more palatable for C++ developers. The C++/CX language extension is perhaps the most ambitious thus far by the Visual C++ team. Ironically, these efforts to improve Standard C++ support have left C++/CX in the dust and really make a language extension redundant.

To prove this point I’m going to show you how to implement IUnknown and IInspectable entirely in modern C++. There is nothing modern or appealing about these two beauties. IUnknown continues to be the central abstraction for prominent APIs like DirectX. And these interfaces—with IInspectable deriving from IUnknown—sit at the heart of the Windows Runtime. I’m going to show you how to implement them without any language extensions, interface tables or other macros—just efficient and elegant C++ with oodles of rich type information that lets the compiler and developer have a great conversation about what needs to be built.

颇具讽刺意味的是,对于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++,就可以让编译器和开发者拥有,关于如何创建所需的,优异的人机对话.

The main challenge is to come up with a way to describe the list of interfaces that a COM or Windows Runtime class intends to implement, and to do so in a way that’s convenient for the developer and accessible to the compiler. Specifically, I need to make this list of types available such that the compiler can interrogate and even enumerate the interfaces. If I can pull that off I might be able to get the compiler to generate the code for the IUnknown QueryInterface method and, optionally, the IInspectable GetIids method, as well. It’s these two methods that pose the biggest challenge. Traditionally, the only solutions have involved language extensions, hideous macros or a lot of hard-to-maintain code.

Both method implementations require a list of interfaces that a class intends to implement. The natural choice for describing such a list of types is a variadic template:

template <typename ... Interfaces>
class __declspec(novtable) Implements : public Interfaces ...
{
};

主要的问题是, 如何列出  COM 或 Windows Runtime 类需要实现的接口, 而且要方便开发者使用, 和编译器访问. 比如, 列出所有可用类型, 以便编译器查询, 甚至枚举出相应的接口. 要是能实现这样的功能, 也许就能让编译器生成 IUnknown QueryInterface 甚至 IInspectable GetIids 方法的代码. 这两个方法才是问题的关键. 按照传统的观念, 唯一的解决办法涉及到语言扩展(language extensions), 万恶的宏定义, 以及一堆难以维护的代码.

两种方法的实现, 都用到类需要实现的接口. 可变参数模板( variadic template)是首选:

template <typename ... Interfaces>
class __declspec(novtable) Implements : public Interfaces ...
{
};

The novtable __declspec extended attribute keeps any constructors and destructors from having to initialize the vfptr in such abstract classes, which often means a significant reduction in code size. The Implements class template includes a template parameter pack, thus making it a variadic template. A parameter pack is a template parameter that accepts any number of template arguments. The trick is that parameter packs are normally used to allow functions to accept any number of arguments, but in this case I’m describing a template whose arguments will be interrogated purely at compile time. The interfaces will never appear in a function parameter list.

One use of those arguments is already plain to see. The param­eter pack expands to form the list of public base classes. Of course, I’m still responsible for actually implementing those virtual functions, but at this point I can describe a concrete class that implements any number of interfaces:

class Hen : public Implements<IHen, IHen2>
{
};

__declspec(novtable)拓展属性可以防止构造函数和析构函数初始化抽象类的vfptr,这通常意味着减少大量的代码.实现类模板包括一个模板参数包,这使它成为一个可变模板.一个参数包即一个模板参数接受任意数目的模板参数变量.但是在这种情况下,我描述的模板参数将只会在编译时进行查询.接口将不会出现在函数的参数列表之中.

这些参数的一个使用已经显而易见.参数包拓展后成为公共基础类的参数列表.当然,我仍然有责任到最后实现这些虚函数,但是此刻我会描述一个实现任意数目接口的一个具体类:

class Hen : public Implements<IHen, IHen2>
{
};

Because the parameter pack is expanded to designate the list of base classes, it’s equivalent to what I might have written myself, as follows:

class Hen : public IHen, public IHen2
{
};

The beauty of structuring the Implements class template in this way is that I can now insert the implementation of various boilerplate code into the Implements class template while the developer of the Hen class can use this unobtrusive abstraction and largely ignore the magic behind it all.

So far, so good. Now I’ll consider the implementation of IUnknown itself. I should be able to implement it entirely inside the Implements class template, given the type of information the compiler now has at its disposal. IUnknown provides two facilities that are as essential to COM classes as oxygen and water are to humans. The first and perhaps simpler of the two is reference counting and is the means by which COM objects track their lifetime. COM prescribes a form of intrusive reference counting whereby each object is responsible for managing its own lifetime based on its awareness of how many outstanding references exist. This is in contrast to a reference counting smart pointer such as the C++11 shared_ptr class template, where the object has no knowledge of its shared ownership. You might argue about the pros and cons of the two approaches, but in practice the COM approach is often more efficient and it’s just the way COM works so you have to deal with it. If nothing else, you’ll likely agree that it’s a horrible idea to wrap a COM interface inside a shared_ptr!

因为参数包拓展为指定基础类的列表,所有它等同于下面我可能会写出的代码:

class Hen : public IHen, public IHen2
{
};

用这种方式结构化实现类模板的美妙之处在于,我现在可以,在实现类模板中,写入各种样版实现代码,而Hen类的开发者则可以使用这种不唐突的抽象,同时大量忽略隐含的细节.

到目前为止,一切都很好.现在,我将考虑IUnknown的实现.我应该可以在实现类模板中完整的实现它,并提供编译器现在所拥有的类型信息.IUnknown提供了对于COM类非常重要的两种工具,就像氧气和水对于人类一样.第一个可能简单些的是引用计数,这也是COM对象跟踪它们生命周期的方式.COM规定一种侵入式的引用计数,它借助于每个对象,统计多少个外部引用存在,来负责管理自己的生命周期.这与智能指针,如C++ 11的shared_ptr类,的引用计数恰恰相反,智能指针对象并不知道它的共享关系.你可能会争论这两种方式的优缺点.但是,实际上COM的方法通常更高效,这也是COM的工作方式,你必须处理它.如果没有其它的,你很可能会同意这点,在shared_ptr里面包装一个COM接口会是一件极不友好的事情!

I’ll begin with the only runtime overhead introduced by the Implements class template:

protected:
  unsigned long m_references = 1;
  Implements() noexcept = default;
  virtual ~Implements() noexcept
  {}

The defaulted constructor isn’t really overhead in itself; it simply ensures the resulting constructor—which will initialize the reference count—is protected rather than public. Both the reference count and the virtual destructor are protected. Making the reference count accessible to derived classes allows for more complex class composition. Most classes can simply ignore this, but do notice that I’m initializing the reference count to one. This is in contrast to popular wisdom that suggests the reference count should initially be zero because no references have been handed out yet. That approach was popularized by ATL and undoubtedly influenced by Don Box’s Essential COM, but it’s quite problematic, as a study of the ATL source code can well attest. Starting with the assumption that the ownership of the reference will immediately be assumed by a caller or attached to a smart pointer provides for a far less error-prone construction process.

我将以只有运行时的开销作为开始,它是通过实现类模板介绍的:

protected:
  unsigned long m_references = 1;
  Implements() noexcept = default;
  virtual ~Implements() noexcept
  {}

默认构造函数并不是真正的开销所在,它只是简单的确保最终的构造函数--它将初始化引用计数--为protected而不是public的.引用计数和虚构造函数都是protected的.让派生类访问引用计数,是为了允许更复杂的类组合.大多数类可以简单的忽略它,但是需要注意的是,我正初始化引用计数为1.这和通常建议初始化引用计数为0,形成鲜明的对比,因为此时并没有处理引用.这个方式在ATL中非常流行,明显受到Don Box的COM本质论的影响,但是这是非常有问题的,ATL的源代码的研究可以作为佐证.开始于这个假设,即引用的所有权将会立即由调用者获得,或者依附于一个提供更少错误构造处理的智能指针.

A virtual destructor is a tremendous convenience in that it allows the Implements class template to implement the reference counting rather than forcing the concrete class itself to provide the implementation. Another option would be to use the curiously recurring template pattern to avoid the virtual function. Normally I’d prefer such an approach, but it would complicate the abstraction slightly, and because a COM class by its very nature has a vtable, there’s no compelling reason to avoid a virtual function here. With these primitives in place, it becomes a simple matter to implement both AddRef and Release inside the Implements class template. First, the AddRef method can simply use the InterlockedIncrement intrinsic to bump up the reference count:

virtual unsigned long __stdcall AddRef() noexcept override
{
  return InterlockedIncrement(&m_references);
}

虚析构函数提供了很大的便利性,它允许实现类模板实现引用计数,而不是强制实现类本身来提供实现.另一个选项,是使用奇特的递归模板模式(Curiously Recurring Template Pattern)来避免使用虚函数.通常我会选择这个方法,但是它会稍微增加抽象的复杂性,同时,因为COM类本身有一个vtable,所以这里也没有什么理由去避免使用虚函数.有了这些基本类型之后,在实现类模板中实现AddRef和Release将会变得非常简单.首先,AddRef方法可以简单的使用InterlockedIncrement来增加引用计数:

virtual unsigned long __stdcall AddRef() noexcept override
{
  return InterlockedIncrement(&m_references);
}

That’s largely self-explanatory. Don’t be tempted to come up with some complex scheme whereby you might conditionally replace the InterlockedIncrement and InterlockedDecrement intrinsic functions with the C++ increment and decrement operators. ATL attempts to do so at a great expense of complexity. If efficiency is your concern, rather spend your efforts avoiding spurious calls to AddRef and Release. Again, modern C++ comes to the rescue with its support for move semantics and its ability to move the ownership of references without a reference bump. Now, the Release method is only marginally more complex:

virtual unsigned long __stdcall Release() noexcept override
{
  unsigned long const remaining = InterlockedDecrement(&m_references);
  if (0 == remaining)
  {
    delete this;
  }
  return remaining;
}

The reference count is decremented and the result is assigned to a local variable. This is important as this result should be returned, but if the object were to be destroyed it would then be illegal to refer to the member variable. Assuming there are no outstanding references, the object is simply deleted via a call to the aforementioned virtual destructor. This concludes reference counting, and the concrete Hen class is still as simple as before:

class Hen : public Implements<IHen, IHen2>
{
};

这不言自明.不要想出某些复杂的方法,通过使用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>
{
};

Now it’s time to consider the wonderful world of QueryInterface. Implementing this IUnknown method is a nontrivial exercise. I cover this extensively in my Pluralsight courses and you can read about the many weird and wonderful ways to roll your own implementation in “Essential COM” (Addison-Wesley Professional, 1998) by Don Box. Be warned that while this is an excellent text on COM, it’s based on C++98 and doesn’t represent modern C++ in any way. For the sake of space and time, I’ll assume you have some familiarity with the implementation of QueryInterface and focus instead on how to implement it with modern C++. Here’s the virtual method itself:

virtual HRESULT __stdcall QueryInterface(
  GUID const & id, void ** object) noexcept override
{
}

Given a GUID identifying a particular interface, QueryInterface should determine whether the object implements the desired interface. If it does, it must increment the reference count for the object and then return the desired interface pointer via the out parameter. If not, it must return a null pointer. Therefore, I’ll start with a rough outline:

*object = // Find interface somehow
if (nullptr == *object)
{
  return E_NOINTERFACE;
}
static_cast<::IUnknown *>(*object)->AddRef();
return S_OK;

现在,到了想象一下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;

So QueryInterface first attempts to find the desired interface somehow. If the interface isn’t supported, the requisite E_NO­INTERFACE error code is returned. Notice how I’ve already taken care of the requirement to clear the resulting interface pointer on failure. You should think of QueryInterface very much as a binary operation. It either succeeds in finding the desired interface or not. Don’t be tempted to get creative here and only conditionally respond favorably. Although there are some limited options allowed by the COM specification, most consumers will simply assume that the interface isn’t supported, regardless of what failure code you might return. Any mistakes in your implementation will undoubtedly cause you no end of debugging misery. QueryInterface is too fundamental to mess around with. Finally, AddRef is called through the resulting interface pointer again to support some rare but permissible class composition scenarios. Those aren’t explicitly supported by the Implements class template, but I’d rather set a good example here. It’s important to keep in mind that the reference-counting operations are interface-specific rather than object-specific. You can’t simply call AddRef or Release on any interface belonging to an object. You must honor the COM rules governing object identity, otherwise you risk introducing illegal code that will break in mysterious ways.

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

返回顶部
顶部