原图
概述
这一章将介绍所谓 policies和 policy classes它们是一种重要的 classes 设计技术,能够增加程序库的弹性并提高复用性,这正是Loki的目标所在。简言之,具备复杂功能的 policy based class由许多小型 classes(称为 policies)组成。每一个这样的小型 class 都只负责单纯如行为或结构的某一方面( behavioral or structural aspect)。一如名称所示,一个 policy会针对特定主题建立个接口。在“遵循 policy接口”的前提下,你可以用任何适当的方法来实作 policies。
由于你可以混合并匹配各种 policies,所以藉由小量核心基础组件( core elementary components)的组合,你可完成一个“行为集”( behaviors set)。
软件设计的多样性(Multiplicity)
让我们考虑一个简単的入门级程序维型:-个 Smart Pointer(智能指针)。这种 class可被用于单线程或多线程之中,可以运用不同的 ownership(拥有权)策略,可以在安全与速度之间协调,可以支持或不支持“自动转为内部指针以上这些特性都可以被自由组合起来而所谓解答,就是最适合你的应用程序的那个方案。
全功能型(Do-It-All)接口的失败
如果程序库将不同的设计实作为各个小型 classes,每个 class代表一个特定的罐装解法,如何?例如在 smart pointer例中你会看到 Singlethreaded Smartptr, Multithreadedsmartptr、Refcountedsmartptr、 Reflinkedsmartptr 等等。 这种做法的问题是会产生大量设计组合。例如上述提到的四个 classes必然导致诸如 singlethreadedrefcountedsmartptr这样的组合。如果再加一个设计选项(例如支持型別转换),会产生更多澘在组合。这最终会让程序库实作者和使用者受不了。很明显地,这并不是个好方法。面对这么多的潜在组合(近乎指数爬升),千万別企图使用暴力枚举法。 这样的程序库不只造成大量的智力负荷,也是极端的严苛而死板。一点点轻微不可测的定制行为(例如试着以一个特定值为预先构造好的 smart pointers设初值)都会导致整个精心制作的 library classes没有用处。 设计是为了厉行 constraints(约束条件、规范)。因此,以设计为目标的程序库必须帮助使用者精巧完成设计,以实现使用者自己的 constraints,而不是实现预先定义好的 constraints。罐装设计不适用于以设计为目标用途的程序库,就像术数字不适用于ー般代码一样。当然啦,一些“最普遍的、受推荐的”罐装解法将受到大众欢迎只要客端程序员必要时能够改变它们。
多重继承( Multiple Inheritance)是救世主?
藉由多重继承机制来组合多项功能,会产生如下问题:
- 关于技术( Mechanics)。目前并没有一成不变即可套用的代码,可以在某种受控情况下将继承而来的 classes组合( assemble)起来。唯一可组合 Basesmartptr, Multithreaded和Refcounted的工具是语言提供的“多重继承”机制:仅仅只是将被组合的 base classes结合在一起并建立一组用来访问其成员的简单规则。除非情况极为单纯,否则结果难以让人接受。大多数时候你得小心协调继承而来的 classes的运转,让它们得到所需的行为。
- 关于型别信息( Type information)。 Based classes并没有足够的型信息来继续完成它们的工作。例如,想象一下,你正试着藉由继承一个 Deepcopy class来为你的 smart pointer实作出深层拷贝( deep copy)。但 Deepcopy应该具有怎样的接口呢?它必须产生一个对象,而其型别目前未知。
- 关于状态处理( State manipulation) base classes实作之各种行为必须操作相同的 state(译注:意指数据)。这意味着他们必须虚继承一个持有该 state的 base class由于总是由user classes继承 library classes(而非反向),这会使设计更加复杂而且变得更没有弹性。
虽然本质上是组合( combinatorial),但多重继承无法单独解决设计时的多样性选择。
Templates带来曙光
templates是一种很适合“组合各种行为”的机制,主要因为它们是“依赖使用者提供的型别信息”并且“在编译期才产生”的代码。 和一般的class不同, class templates可以以不同的方式定制。如果想要针对特定情况来设计class你可以在你的 class template中特化其成员函数来因应。举个例子,如果有一个 Smartart
1 | template <class T, Class U> class Smartptr {...}; |
你可以令 SmartPtr<T,U>针对 widget及其他任意型别加以特化,定义如下:
1 | template <class U> class SmartPtr<Widget, U> {...}; |
template的编译期特性以及“可互相组合”特性,使它在设计期非常引人注目。然而一旦你开始尝试实作这些设计,你会遭遇一些不是那么浅白的问题.
- 你无法特化结构。单单使用 templates,你无法特化“ class 的结构”(我的意思是其数据成员),你只能特化其成员函数。
- 成员函数的特化并不能“依理扩张”。你可以对“单一 template参数”的 class template特化其成员函数,却无法对着“多个 template参数”的 class template特化其个别成员函数。例如:
1 | template <class T> class Widget |
但是,下面是我被告知的错误。
1 | template<class T, class U> class Gadget |
仅对一个成员函数进行部分特化是不可能的,而必须对整个类进行部分特化。这就是C++中的工作方式。
原因是您不能拥有部分专门的功能,而成员功能本身就是功能。通过部分地专用整个类,成员函数将“看起来”像具有较少类型的模板(在该部分专用类中)。
- 程序库撰写者不能够提供多笔缺省值。理想情况下 class template 的作者可以对每个成员函数提供一份缺省实作品,却不能对同一个成员函数提供多份缺省实作品。
现在让我们比较一下多重继承和 templates之间的缺点。有趣的是两者互补。多重继承欠缺技术( mechanics), templates有丰富的技术。多重继承缺乏型别信息,而那东西在 templates里大量存在。 Templates的特化无法扩张( scales),多重继承却很容易扩张。你只能为 template成员函数写一份缺省版本,但你可以写数量无限的 base classes 根据以上分析,如果我们将 templates和多重继承组合起来,将会产生非常具弹性的设备( device),应该很适合用来产生程序库中的“设计元素”( design elements)。
Policies 和 Policy Classes
Policies和 Policy Classes有助于我们设计出安全、有效率且具高度弹性的“设计元素”。所谓 policy,乃用来定义一个 class 或 class template的接口,该接口由下列项目之一或全部组成:内隐型别定义( inner type definition)、成员函数和成员变量
Policies也被其他人用于 traits( Alexandrescu2000a),不同的是后者比较重视行为而非型别。Policies也让人联想到设计模式 Strategy( Gamma et a.195),只不过 policies吃紧于编译期(所谓compile-time bound)。 举个例子,让我们定义一个 policy用以生成对象: Creator policy是一个带有型别T的class template,它必须提供一个名为 Create的函数给外界使用,此函数不接受引数,传回-个 pointer-to-T。就语义而言,每当 Create()被调用就必须传回一个指针,指向新生的T对象。至于对象的精确生成模式( creation mode),留给 policy实作品作为回旋余地。 让我们来定义一个可实作出 Creator policy的 class产生对象的可行办法之一就是表达式new另一个办法是以malloc()加上placement new操作符( Meyers1998b)。此外,还可以采用复制( cloning)方式来产生新对象。下面是三种做法的实呈现:
1 | template <class T> |
任何一个 policy都可以有无限多份实作品。实作出 policy者使称为 policy classes,这个东西并不意图被单独使用,它们主要用于继承或被内含于其他 classes。 这里有一个重要观念: policies接口和一般传统的 classes接口(纯虚函数集)不同,它比较松散,为policy是语法导向( syntax oriented)而非标记导向( signature oriented)換句话说, Creator明确定义的是“怎样的语法构造符合其所规范的class”,而非“必须实作出哪些函数”。例如Creator policy并没有规范 Create()必须是 static还是 virtual,它只要求class必须定义出Create()。此外, Creator也只规定 create)应该(但非必须)传回一个指向新对象的指针因此 Create()也许会传回0或丢出异常一这都极有可能。 面对一个 policy,你可以实作出数个 policy classes它们全都必须遵守 policy所定义的接口。稍后你会看到一个例子,使用者选择了一个 policy并应用到较大结构中。 先前定义的三个 policy classes各有不同的实作方式,甚至连接也有些不同(例如 Prototypecreator多了两个函数 GetPrototype()和 Setprototype()。尽管如此,它们全都定义 Create()并带有必要的返回型別,所以它们都符合 Creator policy 现在让我们看看如何设计一个 class得以利用 Creator policy,它以复合或继承的方式使用先前所定义的三个 classes之,如:
1 | //Library code |
如果 class采用一个或多个 policies,我们称其为 hosts或 host classes。上例的 WidgetManager使是“采用了个 policy'”的 host class. Hosts负责把 policies提供的结构和行为组成一个更复杂的结构和行为。 当客户端将 WidgetManager template具现化( Instantiating)时,必须传进一个他所期望的 policy:
1 | // Application code |
让我们分析整个来龙去脉。无论何时,当个 MyWidgetMgr对象需要产生一个 Widget对象时它便调用它的policy了对象 OpNewCreator
运用 Template Template参数实作 Policy Classes
如同先前例子所示, policy的 template引数往往是余的。使用者每每需要传入 template引数给 Opnewcreator,这很笨拙。一般来说, host class已经知道 policy class所需的参数,或是轻易使可推导出来。上述例子中 WidgetManager总是操作 Widget对象,这种情况下还要求使用者“把 Widget型别传给 Opnewcreator”就显得多余而且危险。 这候程序库可以使用“ template template参数”来描述 policies,如下所示:
1 | // Library code |
尽管露了脸,上述 Created也并未对 WidgetManager有任何贡献。你不能在 WidgetManager中使用 Created,它只是 CreationPolicy(而非 Wi dgetManager)的形式引数( formal argument),此可以省略。 应用端现在只需在使用 WidgetManager时提供 template名称即可:
1 | // Application code |
搭配 policy class使用“ template template参数”,并不单纯只为了方便。有时候这种用法不可或缺,以便 host class可藉由 templates产生不同型别的对象。举个例子,假设 WidgetManager想要以相同的生成策略产生一个 Gadget对象,代码如下:
1 | //Library code |
使用 policies是否会为我们带来一些优势呢?作看之下并不太多。首先, Creator policy的实作木来就十分简短。当然, WidgetManager的作者应该会把“生成对象”的那份代码写成 inline函数,并避开“将 WidgetManager建立为一个 template”时可能遭遇的问题。
然而, policy的确能够带给 WidgetManager非常大的弹性。第一,当你准备具现化( instantiating) WidgetManager时,你可以从外部变更 policies,就和改变 template引数一样简单。第二,你可以根据程序的特殊需求,提供自己的 policies。你可以采用new或malloc或 prototypes或一个专用于你自己系统上的罕见内存分配器。 WidgetManager就像一个小型的“代码生成引擎”( code generation engine),你可以自行设定代码的产生方式。 为了让应用程序开发入员的日子更轻松, WidgetManager的作者应该定义一些常用的 policies,并且以“ template缺省引数”的形式提供最常用的 policy
1 | template <template <class> class Creation=Opnewcreator> |
注意, policies和虚函数有很大不同。虽然虚函数也提供类似效果: class 作者以基本的( primitive)虚函数来建立高端功能,并允许使用者改写这些基本虚函数的行为。然而如前所示, policies因为有丰富的型别信息及静态连接等特性,所以是建立“设计元素”时的本质性东西。不正是“设计”指定了“执行前型别如何互相作用、你能够做什么、不能够做什么”的完整规则吗? Policies可以让你在型别安全( typesafe)的前提下藉由组合各个简单的需求来产出你的设计。此外,由于编译期才将 host class和其 policies结合在一起,所以和手工打造的程序比较起来更加牢固并且更有效率。 当然,也由于 policies的特质,它们不适用于动态连结和二进位接口,所以本质上 policies和传统接口并不互相争。
运用 Template成员函数实作 Policy Classes
另外一种使用“ template template参数”的情况是把 template成员函数用来连接所需的简单类。也就是说,将 policy实作为一般 class(“一般”是相对于 class template而言),但有一个或数个 templated members. 例如,我们可以重新定义先前的 Creator policy成为一个 non-template class,其内提供一个名为 create
1 | struct OpNewCreator |
这种方式所定义并实作出来的 policy,对于旧式编译器有较佳兼容性。但从另一方面来说,这样的 policy难以讨论、定义、实作和运用。
更丰富的 Policies
Creator policy只指定了一个 Create()成员函数。然而 Prototypecreator却多定义了两个函数,分别为 GetPrototype()和 Setprotorype()。让我们来分析一下。由于 WidgetManager继承了 policy class,而且 GetPrototype()和 Setprototype()是Prototype Creator的 public成员,所以这两个函数使被加至 WidgetManager,并且可以直接被使用者取用。 然而 WidgetManager只要求 Create();那是 WidgetManager切所需,也是用来保证它自己机能的唯一要求。不过使用者可以开发出更丰富的接口。 prototype-based Creator policy class的使用者可以写出下列代码:
1 | typedef WidgetManager<Prototypecreator> MyWidqetManager |
如果此后使用者决定采用一个不支持 prototypes的生成策略,那么编译器会指出问题: prototype专属接口已经被用上了。这正是我们希望获得的坚固设计。 如此的结果是很受欢迎的。使用者如果需要扩充 policies,可以在不影响 host class 原本功能的前提下,从更丰富的功能中得到好处。别忘了,决定“哪个 policy被使用”的是使用者而非程序库自身。和般多重接口不同的是, policies给予使用者一种能力,在型别安全( typesafe的前提下扩增 host class的功能.
Policy Classes的析构函数 ( Destructors)
有一个关于建立 policy classes 的要细节。大部分情况下 host class会以 public继承”方式从某些 policies派生而来。因此,使用者可以将一个 host class 自动转为一个 policy class(译注:向上转型),并于稍后 delete该指针。除非 policy class定义了一个虚析构函数( virtual destructor),否则 delete个指向 policy class f的指针,会产生不可预期的结果4,如下所示:
1 | typedef WidgetManager<Prototypecreator> MyWidgetManager; |
然而如果为 policy定义了一个虚析构函数,会妨碍 policy的静态连结特性,也会影响执行效率。
许多 policies并无任何数据成员,纯粹只规范行为。第一个虚函数被加入后会为对象大小带来额外开销(译注:因为引入一份vptr),所以虚析构函数应该尽可能避免。 一个解法是,当 host class 自 policy class派生时,采用 protected继承或 private继承。然而这样会失去丰富的 policies特性。 policies应该采用一个轻便而有效率的解法一定义一个non-virtual protected析构函数.
1 | struct OpNewCreator |
由于析构函数属于 protected层级,所以只有派生而得的 classes才可以摧毁这个 policy对象。这样一来外界就不可能 delete一个指向 policy class 的指针。而由于析构函数并非虚函数,所以不会有大小或速度上的额外开销。
参考文献
《Modern C++ Design-C++设计新思维》