读书笔记 effective c++ Item 35 考虑虚函数的替代者

1. 突破思维——不要将思维限定在面向对象方法上

你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系。你的游戏处在农耕时代,人类很容易受伤或者说健康度降低。因此你决定为其提供一个成员函数,healthValue,返回一个整型值来表明一个人物的健康度。因为不同的人物会用不同的方式来计算健康度,将healthValue声明为虚函数看上去是一个比较明显的设计方式:

1 class GameCharacter {
2 public:
3
4 virtual int healthValue() const; // return character’s health rating;
5
6 ...                                               // derived classes may redefine this
7
8 };     

healthValue没有被声明为纯虚函数的事实表明了会有一个默认的算法来计算健康度(Item 34)。

这的确是设计这个类的一个明显的方式,在某种意义上来说,这也是它的弱点。因为这个设计是如此明显,你可能不会对其他的设计方法有足够的考虑。为了让你逃离面向对象设计之路的车辙,让我们考虑一些处理这个问题的其它方法。

2. 替换虚函数的四种设计方法

2.1 通过使用非虚接口(non-virtual interface(NVI))的模板方法模式

一个很有意思的学派认为虚函数几乎应该总是private的。这个学派的信徒建议一个更好的设计方法是仍然将healthValue声明成public成员函数但是使其变为非虚函数,然后让它调用一个做实际工作的private虚函数,也就是,doHealthValue:

 1 class GameCharacter {
 2 public:
 3 int healthValue() const // derived classes do not redefine
 4 { // this — see Item 36
 5
 6 ...                                       // do “before” stuff — see below
 7
 8 int retVal = doHealthValue(); // do the real work
 9
10 ...                                       // do “after” stuff — see below
11
12 return retVal;
13
14
15 }
16 ...
17 private:
18 virtual int doHealthValue() const // derived classes may redefine this
19 {
20 ... // default algorithm for calculating
21 } // character’s health
22 };

在上面的代码中(这个条款中剩余的代码也如此),我在类定义中展示了成员函数体。正如Item30中所解释的,将其隐式的声明为inline。我使用这种方式的目的只是使你更加容易的看到接下来会发生什么。我所描述的设计和inline之间是独立的,所以不要认为在类内部定义成员函数是有特定意义的,不是如此。、

客户通过public非虚成员函数调用private虚函数的基本设计方法被称作非虚接口(non-virtual interface(NVI))用法。它是更一般的设计模式——模板方法模式(这个设计模式和C++模板没有任何关系)的一个特定表现。我把非虚函数(healthValue)叫做虚函数的一个包装。

NVI用法的一个优点可以从代码注释中看出来,也就是“do before stuff”和“do after stuff”。这些注释指出了在做真正工作的虚函数之前或之后保证要被执行的代码。这意味着这个包装函数在一个虚函数被调用之前,确保了合适的上下文的创建,在这个函数调用结束后,确保了上下文被清除。举个例子,“before”工作可以包括lock a mutex,记录log,验证类变量或者检查函数先验条件是否满足要求,等等。”after”工作可能包含unlocking a mutex,验证函数的后验条件是否满足要求,重新验证类变量等等。如果你让客户直接调用虚函数,那么没有什么好的方法来做到这些。

你可能意识到NVI用法涉及到在派生类中重新定义private虚函数——重新定义它们不能调用的函数!这在设计上并不矛盾。重新定义一个虚函数指定如何做某事,而调用一个虚函数指定何时做某事。这些概念是相互独立的。NVI用法允许派生类重新定义一个虚函数,这使他们可以对如何实现一个功能进行控制,但是基类保有何时调用这个函数的权利。初次看起来很奇怪,但是C++中的派生类可以重新定义继承而来的private虚函数的规则是非常明智的。

对于NVI用法,虚函数并没有严格限定必须为private的。在一些类的继承体系中,一个虚函数的派生类实现需要能够触发基类中对应的部分,如果使得这种调用是合法的,虚函数就必须为protected,而不是private的。有时一个虚函数甚至必须是public的(例如,多态基类中的析构函数——Item7),但是这种情况下,NVI用法就不能够被使用了。

2.2 通过函数指针实现的策略模式

NVI用法是public虚函数的一个很有意思的替换者,但是从设计的角度来说,有一点弄虚作假的嫌疑。毕竟,我们仍然使用了虚函数计算每个人物的健康度。一个更加引人注目的设计方法是将计算一个人物的健康度同这个人物的类型独立开来——这种计算不必作为这个人物的一部分。举个例子,我们可以使用每个人物的构造函数来为健康计算函数传递一个函数指针,然后在函数指针所指的函数中进行实际的运算:

 1 class GameCharacter;                                                                            // forward declaration
 2
 3 // function for the default health calculation algorithm
 4
 5 int defaultHealthCalc(const GameCharacter& gc);
 6
 7 class GameCharacter {
 8
 9 public:
10
11 typedef int (*HealthCalcFunc)(const GameCharacter&);
12
13 explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
14
15 : healthFunc(hcf )
16
17 {}
18
19 int healthValue() const
20
21 { return healthFunc(*this); }
22
23 ...
24
25 private:
26
27 HealthCalcFunc healthFunc;
28
29 };

这个方法是另外一种普通设计模式的简单应用,也就是策略模式。同在GameCharacter继承体系中基于虚函数的方法进行对比,它能提供了一些有意思的灵活性:

  • 相同人物类型的不同实例能够拥有不同的健康度计算函数。举个例子:

    •  1 class EvilBadGuy: public GameCharacter {
       2
       3 public:
       4
       5 explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
       6
       7 : GameCharacter(hcf )
       8
       9 { ... }
      10
      11 ...
      12
      13 };
      14
      15
      16 int loseHealthQuickly(const GameCharacter&); // health calculation
      17 int loseHealthSlowly(const GameCharacter&); // funcs with different
      18 // behavior
      19 EvilBadGuy ebg1(loseHealthQuickly); // same-type charac
      20 EvilBadGuy ebg2(loseHealthSlowly); // ters with different
      21 // health-related
      22 // behavior
  • 特定人物的健康度计算函数能够在运行时发生变化。举个例子,GameCharacter可能提供一个成员函数,setHealthCalculator,它可以对当前的健康度计算函数进行替换。

此外,健康度计算函数不再是GameCharacter继承体系中的成员函数的事实意味着它不能对正在计算健康度的对象的内部数据进行特殊访问。例如,defaultHealthCalc对EvilBadGuy的非public部分没有访问权。如果一个人物的健康度计算仅仅依赖于人物的public接口,这并没有问题,但是如果精确的健康计算需要非public信息,在任何时候当你用类外的非成员非友元函数或者另外一个类的非友元函数来替换类内部的某个功能时,这都会是一个潜在的问题。这个问题在此条款接下来的部分会一直存在,因为我们将要考虑的所有其他的设计方法都涉及到对GameCharacter继承体系外部函数的使用。

作为通用的方法,非成员函数能够对类的非public部分进行访问的唯一方法就是降低类的封装性。例如,类可以将非成员函数声明为友元函数,或者对隐藏起来的部分提供public访问函数。使用函数指针来替换虚函数的优点是否抵消了可能造成的GameCharacter的封装性的降低是你在每个设计中要需要确定的。

 

2.3 通过tr1::function实现的策略模式

 

一旦你适应了模板以及它们所使用的隐式(implicit)接口(Item 41),基于函数指针的方法看起来就非常死板了。为什么健康计算器必须是一个函数而不能用行为同函数类似的一些东西来代替(例如,一个函数对象)?如果它必须是一个函数,为什么不能是一个成员函数?为什么必须返回一个int类型而不是能够转换成Int的任意类型呢?

如果我们使用tr1::funciton对象来替换函数指针的使用,这些限制就会消失。正如Item54所解释的,这些对象可以持有任何可调用实体(也就是函数指针,函数对象,或者成员函数指针),只要它们的签名同客户所需要的相互兼容。这是我们刚刚看到的设计,这次我们使用tr1::function:

 1 class GameCharacter; // as before
 2 int defaultHealthCalc(const GameCharacter& gc); // as before
 3 class GameCharacter {
 4 public:
 5 // HealthCalcFunc is any callable entity that can be called with
 6 // anything compatible with a GameCharacter and that returns anything
 7 // compatible with an int; see below for details
 8 typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
 9
10 explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
11 : healthFunc(hcf )
12 {}
13 int healthValue() const
14 { return healthFunc(*this); }
15 ...
16 private:
17 HealthCalcFunc healthFunc;
18 };

正如你所看到的,HealthCalcFunc是对一个实例化tr1::function的typedef。这意味着它的行为像一个泛化函数指针类型。看看HealthCalcFunc对什么进行了typedef:

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

这里我对这个tr1::function实例的“目标签名”(target signature)做了字体加亮。这个目标签名是“函数带了一个const GameCharacter&参数,并且返回一个int类型”。这个tr1::function类型的对象可以持有任何同这个目标签名相兼容的可调用实体。相兼容的意思意味着实体的参数要么是const GameCharacter&,要么可以转换成这个类型,实体的返回值要么是int,要么可以隐式转换成int。

同上一个设计相比我们看到(GameCharacter持有一个函数指针),这个设计基本上是相同的。唯一的不同是GameCharacter现在持有一个tr1::function对象——一个指向函数的泛化指针。这个改动是小的,但是结果是客户现在在指定健康计算函数上有了更大的灵活性:

 1 short calcHealth(const GameCharacter&);     // health calculation
 2 // function; note
 3 // non-int return type
 4
 5 struct HealthCalculator {                     // class for health
 6
 7
 8 int operator()(const GameCharacter&) const // calculation function
 9 { ... } // objects
10 };
11 class GameLevel {
12 public:
13
14 float health(const GameCharacter&) const;    // health calculation
15
16 ...                                                                 // mem function; note
17
18 };                                                                 // non-int return type
19
20
21
22 class EvilBadGuy: public GameCharacter { // as before
23
24 ...
25
26 };
27
28 class EyeCandyCharacter: public GameCharacter { // another character
29 ... // type; assume same
30
31 };                                                               // constructor as
32 // EvilBadGuy
33
34 EvilBadGuy ebg1(calcHealth);                   // character using a
35 // health calculation
36 // function
37
38 EyeCandyCharacter ecc1(HealthCalculator());          // character using a
39 // health calculation
40 // function object
41
42 GameLevel currentLevel;
43 ...
44 EvilBadGuy ebg2(                                     // character using a
45
46
47 std::tr1::bind(&GameLevel::health, // health calculation
48
49 currentLevel,               // member function;
50
51 _1)                               // see below for details
52
53
54 );

你会因为tr1::function的使用而感到吃惊。它一直让我很兴奋。如果你不感到兴奋,可能是因为刚开始接触ebg2的定义,并且想知道对tr1::bind的调用会发生什么。看下面的解释:

我想说为了计算ebg2的健康度,应该使用GameLevel类中的健康成员函数。现在,GameLevel::health是一个带有一个参数的函数(指向GameCharacter的引用),但是它实际上有两个参数,因为它同时还有一个隐含的GameLevel参数——由this所指向的。然而GameCharacters的健康计算函数却只有一个参数:也就是需要计算健康度的GameCharacter。如果我们对ebg2的健康计算使用GameLevel::health,我们必须做一些“适配”工作,以达到只带一个参数(GameCharacter)而不是两个参数(GameCharacter和GameLevel)的目的。在这个例子中,我们想使用GameLevel对象currentLevel来为ebg2计算健康度,所以我们每次使用”bind”到currentLevel的GameLevel::health函数来计算ebg2的健康度。这也是调用tr1::bind所能做到的:它指定了ebg2的健康计算函数应该总是使用currentLevel作为GameLevel对象。

我跳过了tr1::bind调用的很多细节,因为这样的细节不会有很多启发意义,并且会分散我要强调的基本观点:通过使用tr1::function而不是一个函数指针,当计算一个人物的健康度时我们可以允许客户使用任何兼容的可调用实体。这是不是很酷。

2.4 “典型的”策略模式

如果你对设计模式比上面的C++之酷更有兴趣,策略模式的一个更加方便的方法是将健康计算函数声明为一个独立健康计算继承体系中的虚成员函数。最后的继承体系设计会是下面的样子:

如果你对UML符号不熟悉,上面的UML图说明的意思是GameCharacter是继承体系中的root类,EvilBadGuy和EyeCandyCharacter是派生类;HealthCalcFunc是root类,SlowHealthLoser和FastHealthLoser是派生类;每个GameCharacter类型都包含了一个指向HealthCalcFunc继承体系对象的指针。

下面是代码的骨架:

 1 class GameCharacter;                                                                      // forward declaration
 2
 3 class HealthCalcFunc {
 4
 5 public:
 6
 7 ...
 8
 9 virtual int calc(const GameCharacter& gc) const
10
11 { ... }
12
13 ...
14
15 };
16
17 HealthCalcFunc defaultHealthCalc;
18
19 class GameCharacter {
20
21 public:
22
23 explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
24
25 : pHealthCalc(phcf)
26
27 {}
28
29 int healthValue() const
30
31 { return pHealthCalc->calc(*this); }
32
33 ...
34
35 private:
36
37 HealthCalcFunc *pHealthCalc;
38
39 };  

很容易识别出来这是人们所熟知的”标准”策略模式的实现,它也为现存的健康计算算法的调整提供了可能性,你只需要添加一个HealthCalcFunc的派生类就可以了。

2.5 替换方法总结

这个条款的基本建议是当为你所要解决的问题寻找一个设计方法时,考虑一下虚函数设计的替代方法。下面是我们介绍的设计方法回顾:

  • 使用非虚接口用法(NVI idiom),这是模板方法设计模式(Template Method design pattern),它用public非虚成员函数来包裹更低访问权的虚函数来实现。
  • 用函数指针成员函数来替代虚函数,这是策略设计模式的分解表现形式。
  • 用tr1::function数据成员来代替虚函数,它可以使用同目标签名(signature)相兼容的任何可调用实体。这也是策略设计模式的一种形式。
  • 将一个继承体系中的虚函数替换为另外一个继承体系的虚函数。这是策略设计模式的传统实现方法。

这并不是替换虚函数的所有设计方法,但是应该足够使你确信这些方法是确实存在的。进一步来说,它们的优缺点使你更加清楚你应该考虑使用它们。

为了避免在面向对象设计的路上被卡住,你需要时不时的拉一把。有很多其他的方法。值得我们花时间去研究它们。

7. 总结

    • 虚函数的替换方法包括NVI用法和策略设计模式的其他不同的形式。NVI用法本身是模板方法设计模式的一个例子。
    • 将功能从成员函数移到类外函数的一个缺点是非成员函数不能再访问类的非public成员。
    • Tr1::function对象的行为就像一个泛化函数指针。这种对象支持同给定目标签名相兼容的所有可调用实体。
时间: 2024-12-19 17:47:00

读书笔记 effective c++ Item 35 考虑虚函数的替代者的相关文章

读书笔记 effective c++ Item 45 使用成员函数模板来接受“所有兼容类型”

智能指针的行为像是指针,但是没有提供加的功能.例如,Item 13中解释了如何使用标准auto_ptr和tr1::shared_ptr指针在正确的时间自动删除堆上的资源.STL容器中的迭代器基本上都是智能指针:当然,你不能通过使用“++”来将链表中的指向一个节点的内建指针移到下一个节点上去,但是list::iterator可以这么做. 1. 问题分析——如何实现智能指针的隐式转换 真正的指针能够做好的一件事情是支持隐式转换.派生类指针可以隐式转换为基类指针,指向非const的指针可以隐式转换成为

Effective C++ Item 35 考虑 virtual 函数以外的实现

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.virtual 函数版本 class GameCharacter{ public: virtual int healthValue() const; //返回人物的健康指数, derived classes 可重新定义它 }; 2.使用 non-virtual interface 手法,那是 Template Method 设计模式的一种特殊形式. 让客户通过 public non-v

读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C++标准预计在2009年被发布(虽然所有的工作很有可能在2007年底被完成).直到现在,发布下一版C++的预计年份还没有被确定,这就解释了为什么人们把下一版C++叫做“C++0x”——C++的200x年版本. C++0x可能会包含一些有趣的新的语言特性,但是大多数新C++功能将会以标准库附加物的形式被

读书笔记 effective c++ Item 43 了解如何访问模板化基类中的名字

1. 问题的引入——派生类不会发现模板基类中的名字 假设我们需要写一个应用,使用它可以为不同的公司发送消息.消息可以以加密或者明文(未加密)的方式被发送.如果在编译阶段我们有足够的信息来确定哪个信息会被发送到哪个公司,我们可以使用基于模板的解决方案: 1 class CompanyA { 2 public: 3 ... 4 void sendCleartext(const std::string& msg); 5 void sendEncrypted(const std::string&

读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追求还是不减,应该是感动了周公吧,梦境从此处开始,大师入场来给我安慰了... 11点躺在床上了,脑子里总结一下最近的工作:最近的开发用到inline函数比较多,众所周知,inline的使用是为了提高程序性能,可结果却总不尽如人意,这个捉急啊,嗯?怎么突然到了山脚下,周边树木林立,郁郁葱葱,鸟儿委婉啼叫

读书笔记 effective c++ Item 53 关注编译器发出的警告

许多程序员常常忽略编译器发出的警告.毕竟,如果问题很严重,它才将会变成一个error,不是么?相对来说,这个想法可能在其它语言是无害的,但是在C++中,我敢打赌编译器的实现者对于对接下来会发生什么比你有更好的理解.例如,下面的错误是每个人都时不时会犯的: 1 class B { 2 public: 3 virtual void f() const; 4 }; 5 class D: public B { 6 public: 7 virtual void f(); 8 }; 1. 你对警告的理解可能

读书笔记 effective c++ Item 41 理解隐式接口和编译期多态

1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), 1 class Widget { 2 public: 3 Widget(); 4 virtual ~Widget(); 5 6 virtual std::size_t size() const; 7 virtual void normalize(); 8 9 void swap(Widget& other); // see Item 25 10 11 ... 12 13 }; 考虑下

读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定

Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些并不直观,所以知道这些规则是什么很重要. 1. 定义operator new的约定 1.1 约定列举 我们以operator new开始.实现一个一致的operator new需要有正确的返回值,在没有足够内存的时候调用new-handling函数(见Item 49),并且做好准备处理没有内存可分配

读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声明,编译器同样会为你声明一个默认拷贝构造函数.这些所有的函数会是public和inline的(Item30).因此,如果你写了下面的类: 1 class Empty{}; 本质上来说和下面的类是一样的: 1 class Empty { 2 3 public: 4 5 Empty() { ... }