条款35:考虑virtual函数以外的其他选择
在这个条款里面讨论virtual函数的替代方案。文中主要探讨了两种方式——NVI手法(Template Method模式)和Strategy模式。
一、NVI手法
NVI即Non-Virtual Interface。是Template Method设计模式中特定的一种,算法骨架来自于基类,具体的实现是在子类中实现。
在某个流派中,他们建议virtual函数几乎总是private的,然后使用一个non-virtual的member函数来调用virtual函数。
class GameCharacter
{
public:
int healthValue() const
{
... // 事前工作
int retVal = doHealthValue();
... // 事后工作
}
private:
virtual int doHealthValue()
{
...
}
};
作者把上述的那个non-virtual函数——healthValue称为virtual函数的外覆器。
NVI手法的一个优点在于在调用外覆器之前设定好适当的场景,调用之后清理场景。 个人认为,在这个过程中要注意异常的抛出,要处理好异常。否则,事前如果锁定互斥锁,接着执行virtual函数的时候抛出了异常,那么这个锁要怎么释放呢?
二、古典的Strategy模式
采用函数指针作为计算健康值的计算方法。不作为member函数。
UML图类似如下:
在这个例子的大致做法就是,给定一个non-member的计算健康值的函数,作为Character的构造函数参数,然后使用这个函数去计算健康值。
因为这边涉及到设计模式的方法,所以没有很细致的记录下来,会在后面学习设计模式的博客中分析设计模式。现在,让我们来总结一下替代方案:
- NVI手法。 以public non-virtual成员函数包裹较低访问性(private or protected)的virtual函数。
- 将virtual函数替换为“函数指针成员变量”。这是Strategy模式的一种分解表现形式。
- 以std::tr1::function替换virtual函数。 这也是Strategy模式的一种分解形式。
- 将继承体系中的virtual函数替换为另一个继承体系内的virtual函数。 这是Strategy模式的传统实现手法。
作者总结
virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
条款36:绝不重新定义继承而来的non-virtual函数
这一条款其实很简单地就能陈述其原因。先肯定地明确一点:non-virtual函数的不变性凌驾于特异性。 继承一个non-virtual函数,使用public继承就是想要继承一个接口和强制性实现,所以不变性凌驾于特异性。
然后我们再看一眼适合讲述的例子:
class B
{
public:
void mf();
};
class D : pubic B
{
public:
void mf();
};
D以public形式继承了B,且重写了一份non-virtual的mf函数。那么B中的non-virtual就会被覆盖。考虑以下执行:
D x;
B *pB = &x;
D *pD = &x;
pB->mf(); // 调用B::mf
pD->mf(); // 调用D::mf
现在,同一个x,调用的肯定会是不同的mf函数实现。这是因为non-virtual函数都是静态绑定的,不是动态绑定,他们会调用各自指针指向类的成员函数。
所以,如果真要有一份不同的mf函数的实现,那么我们就应该考虑其声明为virtual函数。
作者总结
绝对不要重新定义继承而来的non-virtual函数。
条款37:绝不重新定义继承而来的缺省参数值
一、原因
virtual函数是动态绑定(dynamically bound)的,缺省参数值却是静态绑定的(statically bound)。
二、从代码中看到错误
#include <iostream>
using namespace std;
class Shape
{
public:
enum ShapeColor
{
Red,
Green,
Blue
};
virtual void draw(ShapeColor color = Red) = 0; // 默认参数是Red
inline void ShowColor(ShapeColor color)
{
if (color == 0)
{
cout << "Red" << endl;
}
else if (color == 1)
{
cout << "Green" << endl;
}
else if (color == 2)
{
cout << "Blue" << endl;
}
else
{
cout << "invalid color" << endl;
}
}
};
class Rectangle : public Shape
{
public:
virtual void draw(ShapeColor color = Green) // 默认参数是Green
{
cout << "default parameter : Green, But the real parameter:";
ShowColor(color);
}
};
class Circle : public Shape
{
public:
virtual void draw(ShapeColor color = Blue) // 默认参数是Blue
{
cout << "default parameter : Blue , But the real parameter:";
ShowColor(color);
}
};
class Triangle : public Shape
{
public:
virtual void draw(ShapeColor color) // 默认参数是Blue
{
cout << "No default parameter" << endl;
ShowColor(color);
}
};
int main()
{
Shape *pR = new Rectangle;
Shape *pC = new Circle;
Shape *pT = new Triangle;
pR->draw();
pC->draw();
pT->draw(Shape::Green);
return 0;
}
七十来行的代码,讲一下重点:
(1) 抽象基类中有一个纯虚函数draw,缺省参数值为Red。
(2) Rectangle重写draw,默认参数值为Green。
(3) Circle的重写draw,默认参数值为Blue。
(4) Triangle重写draw,没有默认参数值。
(5) 接下来我们用一个Shape类的指针,分别指向三个继承类。
(6) 然后分别调用他们的draw函数。
讲道理的话,pR调用的应该是自己的draw函数,默认参数为Green,同理,pC的默认参数为Blue。事实上的执行结果:
三、缺省参数值是静态绑定的
以上的例子已经完美说明了,virtual是动态绑定的,而缺省参数值却是静态绑定的。
- 因为virtual是动态绑定的,所以指向derived classes的base指针会寻找到正确的virtual函数去执行。
- 因为默认参数值是静态绑定的,所以使用的缺省参数值还是base类的缺省参数Red。
四、为何支持这种看似错误的方式?
如果编译器没有给我们这样的默认参数一个错误或者警告提示,为何不像我们想的那样,将缺省参数值也进行动态绑定呢?
为了运行期效率。 如果编译器要执行某种方法将运行期的virtual的函数决定适当的缺省参数值,比现在这种“在编译器决定”的机制更加复杂且更加慢。
作者总结
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定的。