首先明确下,虚函数是动态绑定,缺省参数值是静态绑定
// a class for geometric shapes class Shape { public: enum ShapeColor { Red, Green, Blue }; // all shapes must offer a function to draw themselves virtual void draw(ShapeColor color = Red) const = 0; ... }; class Rectangle: public Shape { public: // notice the different default parameter value — bad! virtual void draw(ShapeColor color = Green) const; ... }; class Circle: public Shape { public: virtual void draw(ShapeColor color) const; ... };
当我们这样指定这些指针时。
Shape *ps; // static type = Shape* Shape *pc = new Circle; // static type = Shape* Shape *pr = new Rectangle; // static type = Shape*
三个对象的静态类型均为Shape*,这时调用它们的虚函数:
pc->draw(Shape::Red); // calls Circle::draw(Shape::Red) pr->draw(Shape::Red); // calls Rectangle::draw(Shape::Red)
由于虚函数是动态绑定的,所以pc,pr调用各自的draw没错,但是却是用的时shape中draw指定的Red参数值,当运行这段代码时,画出来的图像可能就是个四不像。
所以如果基类成员虚函数中定义了缺省值,就不能用常规的方法来使用它,我们需要考虑它的替代设计:
class Shape { public: enum ShapeColor { Red, Green, Blue }; void draw(ShapeColor color = Red) const // now non-virtual { doDraw(color); // calls a virtual } ... private: virtual void doDraw(ShapeColor color) const = 0; // the actual work is }; // done in this func
class Rectangle: public Shape { public: ... private: virtual void doDraw(ShapeColor color) const; // note lack of a ... // default param val. };
派生类可以重新定义虚函数,而且可以指定自己独有的color,而non-virtual函数draw按照c++设计原则是不应该被重写的。这种方法称为non-virtual interface,但是有一些函数根据实际需求必须是public,比方多态性质的基类析构函数,这样一来就不能使用NVI了。
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; : healthFunc(hcf ) {} int healthValue() const { return healthFunc(*this); } ... private: HealthCalcFunc healthFunc; };
上例中我们把HealthCalcFunc声明为一个typedef,他接受一个GameCharacter引用类型的参数,返回int.tr1::function的功能是使可调物的参数隐式的转换为const GameCharacter&,同理返回类型隐式的转换为int。
我们可以调用改函数进行计算,使用这种方式来替代虚函数的设计避免了NVI的问题,又展现了出色的兼容性。
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 );
为了计算ebg2的健康指数,应该使用GameLevel class的成员函数health。GameLevel::haelth宣称它自己接受一个参数(那是个引用指向GameCharacter),但它实际上接受两个参数,因为它也获得了一个隐式参数gameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter(这个对象将被计算出健康指数)。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter和一个GameLevel),转而接受单一参数(一个GameCharacter)。这个例子中我们必然会想要使用currentLevel作为“ebg2的健康计算函数所需的那个GameLevel对象”,于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为:它指出ebg2的健康计算函数总是以currentLevel作为GameLevel对象。
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; };
这个设计方案是将virtual函数替换为另一个继承体系中的virtual函数。