AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > C++

C++箴言:考虑可选的虚拟函数的替代方法

51自学网 http://www.wanshiok.com

  经由 tr1::function 实现的策略模式

  一旦你习惯了 templates(模板)和 implicit interfaces(隐式接口)的应用,function-pointer-based(基于函数指针)的方法看上去就有些死板了。健康值的计算为什么必须是一个 function(函数),而不能是某种简单的行为类似 function(函数)的东西(例如,一个 function object(函数对象))?如果它必须是一个 function(函数),为什么不能是一个 member function(成员函数)?为什么它必须返回一个 int,而不是某种能够转型为 int 的类型?

  如果我们用一个 tr1::function 类型的对象代替一个 function pointer(函数指针)(诸如 healthFunc),这些约束就会消失。这样的对象可以持有 any callable entity(任何可调用实体)(例如,function pointer(函数指针),function object(函数对象),或 member function pointer(成员函数指针)),这些实体的标志性特征就是兼容于它所期待的东西。我们马上就会看到这样的设计,这次使用了 tr1::function:

class GameCharacter;                                 // as before
int defaultHealthCalc(const GameCharacter& gc);      // as before

class GameCharacter {
public:
   // HealthCalcFunc is any callable entity that can be called with
   // anything compatible with a GameCharacter and that returns anything
   // compatible with an int; see below for details
   typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
   explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
   : healthFunc(hcf)
   {}

   int healthValue() const
   { return healthFunc(*this);   }

   ...

private:
  HealthCalcFunc healthFunc;
};

  就像你看到的,HealthCalcFunc 是一个 tr1::function instantiation(实例化)的 typedef。这意味着它的行为类似一个普通的 function pointer(函数指针)类型。我们近距离看看 HealthCalcFunc 究竟是一个什么东西的 typedef:

std::tr1::function<int (const GameCharacter&)>

  这里我突出了这个 tr1::function instantiation(实例化)的“target signature(目标识别特征)”。这个 target signature(目标识别特征)是“取得一个引向 const GameCharacter 的 reference(引用),并返回一个 int 的函数”。这个 tr1::function 类型的(例如,HealthCalcFunc 类型的)对象可以持有兼容于这个 target signature(目标识别特征)的 any callable entity(任何可调用实体)。兼容意味着这个实体的参数能够隐式地转型为一个 const GameCharacter&,而它的返回类型能够隐式地转型为一个 int。

  与我们看到的最近一个设计(在那里 GameCharacter 持有一个指向一个函数的指针)相比,这个设计几乎相同。仅有的区别是目前的 GameCharacter 持有一个 tr1::function 对象——指向一个函数的 generalized(泛型化)指针。除了达到“clients(客户)在指定健康值计算函数时有更大的灵活性”的效果之外,这个变化是如此之小,以至于我宁愿对它视而不见:

short calcHealth(const GameCharacter&);          // health calculation
                                                 // function; note
                                                 // non-int return type

struct HealthCalculator {                        // class for health
  int operator()(const GameCharacter&) const     // calculation function
  { ... }                                        // objects
};

class GameLevel {
public:
  float health(const GameCharacter&) const;      // health calculation
  ...                                            // mem function; note
};                                               // non-int return type


class EvilBadGuy: public GameCharacter {         // as before
  ...
};
class EyeCandyCharacter:   public GameCharacter {  // another character
  ...                                              // type; assume same
};                                                 // constructor as
                                                   // EvilBadGuy


EvilBadGuy ebg1(calcHealth);                       // character using a
                                                   // health calculation
                                                   // function


EyeCandyCharacter ecc1(HealthCalculator());        // character using a
                                                   // health calculation
                                                   // function object

GameLevel currentLevel;
...
EvilBadGuy ebg2(                                   // character using a
  std::tr1::bind(&GameLevel::health,               // health calculation
          currentLevel,                            // member function;
          _1)                                      // see below for details
);

  就个人感觉而言:我发现 tr1::function 能让你做的事情是如此让人惊喜,它令我浑身兴奋异常。如果你没有感到兴奋,那可能是因为你正目不转睛地盯着 ebg2 的定义并对 tr1::bind 的调用会发生什么迷惑不解。请耐心地听我解释。

  比方说我们要计算 ebg2 的健康等级,应该使用 GameLevel class(类)中的 health member function(成员函数)。现在,GameLevel::health 是一个被声明为取得一个参数(一个引向 GameCharacter 的引用)的函数,但是它实际上取得了两个参数,因为它同时得到一个隐式的 GameLevel 参数——指向 this。然而,GameCharacters 的健康值计算函数只取得单一的参数:将被计算健康值的 GameCharacter。如果我们要使用 GameLevel::health 计算 ebg2 的健康值,我们必须以某种方式“改造”它,以使它适应只取得唯一的参数(一个 GameCharacter),而不是两个(一个 GameCharacter 和一个 GameLevel)。在本例中,我们总是要使用 currentLevel 作为 GameLevel 对象来计算 ebg2 的健康值,所以每次调用 GameLevel::health 计算 ebg2 的健康值时,我们就要 "bind"(凝固)currentLevel 来作为 GameLevel 的对象来使用。这就是 tr1::bind 的调用所做的事情:它指定 ebg2 的健康值计算函数应该总是使用 currentLevel 作为 GameLevel 对象。

  我们跳过一大堆的细节,诸如为什么 "_1" 意味着“当为了 ebg2 调用 GameLevel::health 时使用 currentLevel 作为 GameLevel 对象”。这样的细节并没有什么启发性,而且它们将转移我所关注的基本点:在计算一个角色的健康值时,通过使用 tr1::function 代替一个 function pointer(函数指针),我们将允许客户使用 any compatible callable entity(任何兼容的可调用实体)。很酷是不是?

  “经典的”策略模式

  如果你比 C++ 更加深入地进入 design patterns(设计模式),一个 Strategy 的更加习以为常的做法是将 health-calculation function(健康值计算函数)做成一个独立的 health-calculation hierarchy(健康值计算继承体系)的 virtual member function(虚拟成员函数)。做成的 hierarchy(继承体系)设计看起来就像这样:


  如果你不熟悉 UML 记法,这不过是在表示当把 EvilBadGuy 和 EyeCandyCharacter 作为 derived classes(派生类)时,GameCharacter 是这个 inheritance hierarchy(继承体系)的根;HealthCalcFunc 是另一个带有 derived classes(派生类)SlowHealthLoser 和 FastHealthLoser 的 inheritance hierarchy(继承体系)的根;而每一个 GameCharacter 类型的对象包含一个指向“从 HealthCalcFunc 派生的对象”的指针。

  这就是相应的框架代码:

class GameCharacter;                            // forward declaration

class HealthCalcFunc {
public:

  ...
  virtual int calc(const GameCharacter& gc) const
  { ... }
  ...

};

HealthCalcFunc defaultHealthCalc;

class GameCharacter {
public:
  explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
  : pHealthCalc(phcf)
  {}

  int healthValue() const
  { return pHealthCalc->calc(*this);}

  ...

private:
  HealthCalcFunc *pHealthCalc;
};

  这个方法的吸引力在于对于熟悉“标准的”Strategy pattern(策略模式)实现的人可以很快地识别出来,再加上它提供了通过在 HealthCalcFunc hierarchy(继承体系)中增加一个 derived class(派生类)而微调已存在的健康值计算算法的可能性。

  小结

  本文的基本建议是当你为尝试解决的问题寻求一个设计时,你应该考虑可选的 virtual functions(虚拟函数)的替代方法。以下是对我们考察过的可选方法的一个简略的回顾:

  • 使用 non-virtual interface idiom (NVI idiom)(非虚拟接口惯用法),这是用 public non-virtual member functions(公有非虚拟成员函数)包装可访问权限较小的 virtual functions(虚拟函数)的 Template Method design pattern(模板方法模式)的一种形式。
  • function pointer data members(函数指针数据成员)代替 virtual functions(虚拟函数),一种 Strategy design pattern(策略模式)的显而易见的形式。
  • tr1::function data members(数据成员)代替 virtual functions(虚拟函数),这样就允许使用兼容于你所需要的东西的 any callable entity(任何可调用实体)。这也是 Strategy design pattern(策略模式)的一种形式。
  • virtual functions in another hierarchy(另外一个继承体系中的虚拟函数)代替 virtual functions in one hierarchy(单独一个继承体系中的虚拟函数)。这是 Strategy design pattern(策略模式)的习以为常的实现。

  这不是一个可选的 virtual functions(虚拟函数)的替代设计的详尽无遗的列表,但是它足以使你确信这些是可选的方法。此外,它们之间互为比较的优劣应该使你考虑它们时更为明确。

  为了避免陷入 object-oriented design(面向对象设计)的习惯性道路,时不时地给车轮一些有益的颠簸。有很多其它的道路。值得花一些时间去考虑它们。

  Things to Remember

  • 可选的 virtual functions(虚拟函数)的替代方法包括 NVI 惯用法和 Strategy design pattern(策略模式)的各种变化形式。NVI 惯用法本身是 Template Method design pattern(模板方法模式)的一个实例。
  • 将一个机能从一个 member function(成员函数)中移到 class(类)之外的某个函数中的一个危害是 non-member function(非成员函数)没有访问类的 non-public members(非公有成员)的途径。
  • tr1::function 对象的行为类似 generalized function pointers(泛型化的函数指针)。这样的对象支持所有兼容于一个给定的目标特征的 callable entities(可调用实体)。

 
 

上一篇:C++之父Bjarne谈C++的未来发展  下一篇:C++对象布局及多态实现探索之内存布局