六、继承与面向对象设计--条款35-37

条款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函数——你唯一应该覆写的东西——却是动态绑定的。

posted @ 2018-09-25 14:41  _NewMan  阅读(250)  评论(0编辑  收藏  举报