条款34: 区分接口继承和实现继承
Differentiate between inheritance of interface and inheritance of implementation表面上直截了当的 public 继承概念,经过更严密的检查之后,发现它由两部分组成: 函数接口(function interface)继承和函数实现(function implementation)继承.这两种继承的差异,很像函数声明和函数定义之间的差异.
身为 class 设计者,有时候希望derived class 只继承成员函数的接口(也就是声明);有时候又会希望derived class 同时继承函数的接口和实现,但又希望能够覆写它们所继承的实现;有时候希望derived class 同时继承函数的接口和实现,并且不允许覆写任何东西.
考虑下面的 class 继承体系:
class Shape {
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
...
};
class Rectangle : public Shape { ... };
class Ellipse : public Shape { ... };
Shape是个抽象 class:它的pure virtual 函数draw使它成为一个抽象 class.所以客户不能够创建Shape class 的实体,只能够创建其derived class 的实体.尽管如此,Shape还是强烈影响了所有以 public 形式继承它的derived class,因为:
1.成员函数的接口总是会被继承.正如条款32所述,public 继承意味着is-a,所以对base class 为真的任何事情一定对其derived class 为真.因此如果某个函数可施行于某 class 身上,一定也可施行于其derived class 身上.
Shape class 声明了三个函数,每个函数的声明形式都不相同:draw是个pure virtual 函数;error是个简朴的impure virtual 函数;objectID是个non-virtual 函数.这些不同的声明带来什么样的暗示呢?
首先考虑pure virtual 函数draw:
class Shape {
public:
virtual void draw() const = 0;
...
};
pure virtual 函数有两个最突出的特性:它们必须被任何"继承了它们"的具象 class 重新声明,而且它们在抽象 class 中通常没有定义.把这两个形式摆在一起,就会发现:
2.声明一个pure virtual 函数的目的是为了让derived class 只继承函数接口.
这对Shape::draw函数是再合理不过的事,因为所有Shape对象都应该是可绘出的,这是合理的要求.但Shape class 无法为此函数提供合理的缺省实现,因为不同的图形绘制方法不同.Shape::draw的声明式是对具象derived class 设计者说"必须提供一个draw函数,但不干涉怎么实现它."
令人意外的是,竟然可以为pure virtual 提供定义,也就是说可以为Shape::draw提供一份实现代码,C++并不会发出警告,但调用它的唯一途径是"调用时明确指出其class名称":
Shape* ps = new Shape; // error,Shape是抽象的
Shape* ps1 = new Rectangle; // ok
ps1->draw(); // ok,调用Rectangle::draw
Shape* ps2 = new Ellipse; // ok
ps2->draw(); // ok,调用Ellipes::draw
ps1->Shape::draw(); // ok,调用Shape::draw
ps2->Shape::draw(); // ok,调用Shape::draw
它可以实现一种机制,为简朴的impure virtual 函数提供更平常更安全的缺省实现.
简朴的impure virtual 函数背后的事情和pure virtual 函数有点不同.derived class 继承其函数接口,但impure virtual 函数会提供一份实现代码,derived class 可能覆写它.
3.声明简朴的(非纯)impure virtual 函数的目的,是让derived class 继承该函数的接口和缺省实现.
考虑Shape::error这个例子:
class Shape {
public:
virtual void error(const std::string& msg);
...
};
其接口表示,每个 class 都必须支持一个"当遇上错误时可调用"的函数,但每个 class 可自由处理错误.如果某个 class 不想针对错误做出任何特殊行为,它可以退回到Shape class 提供的缺省错误处理行为.也就是说Shape::error的声明告诉derived class 的设计者,"必须支持一个error函数,但如果不想自己写一个,可以使用Shape class 提供的缺省版本".
但是,允许impure virtual 函数同时指定函数声明和函数缺省行为,却可能造成危险.考虑如下继承体系,XYZ航空公司,只有A型和B型两种飞机,两者以相同方式飞行,因此:
<pre name="code" class="cpp">class Airport { ... };
class Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination) {
// 缺省代码,将飞机飞至指定的目的地
}
class ModelA : public Airplane { ... };
class ModelB : public Airplane { ... };
两个 class(ModelA和ModelB)共享一份相同性质,所以共同性质被搬到base class 中,然后被这两个 class 继承. 假设XYZ又新购买C型飞机,它和以前的飞行方式不同.如果仍然以以前的方式继承:class ModelC : public Airplane { ... }; 则造成错误,因为ModelC的fly函数应该与ModelA有所不同.
问题就不在Airplane::fly有缺省行为,而在于ModelC在未说明出"我要"的情况下就继承了该缺省行为.幸运的是可以轻易做到"提供缺省实现给derived class,但除非它们明白要求,否则免谈".此间
伎俩在于切断"virtual函数接口"和其"缺省实现"之间的连接.下面是一种做法:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination) {
// 缺省行为,将飞机飞至指定的目的地
}
注意,Airplane::fly已被改为一个pure virtual 函数,只提供飞行接口.其缺省行为也出现在Airplane class 中,但以独立函数defaultFly的姿态出现.若想使用缺省实现,可以在其fly函数中对defaultFly做一个 inline 调用(注意
条款30所言,inline 函数和 virtual 函数之间的交互关系):
<pre name="code" class="cpp">class ModelA : public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
...
};
class ModelB : public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
...
};
现在ModelC class 不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual 函数迫使ModelC必须提供自己的fly版本:
<pre name="code" class="cpp">class ModelC : public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination) {
// 将C型飞机飞至指定的目的地
}
最后,看看Shape的non-virtual 函数objectID:
class Shape {
public:
int objectID() const;
...
};
如果成员函数是个non-virtual 函数,意味它并不打算在derived class 中有不同的行为.
4.声明non-virtual 函数的目的是为了令derived class 继承函数的接口以及一份强制性实现.
pure virtual 函数,simple(impure) virtual 函数,non-virtual 函数之间的差异,使得可以精确指定想要derived class 继承的东西:只继承接口,或继承接口和一份缺省实现,或继承接口和一份强制实现.
class 设计者最常犯的两个错误:
第一个错误是将所有函数声明为non-virtual .这使得derived class 没有多余空间进行特化工作.non-virtual 析构函数尤其会带来问题(详见 条款7).当然,设计一个并不想成为base class 的 class 是合理的,都是non-virtual 函数是正确的.但如果一个base被用来做base class,都会拥有若干 virtual 函数(详见 条款7).
第二个错误是将所哟偶成员声明为 virtual.有时候这样是正确的,例如条款31的Interface class.但这也可能是 class 设计者缺乏坚定立场的前兆,某些函数就是不应该在derived class 中被重新定义,如果那样就必须将那些函数声明为non-virtual.
注意:
接口继承和实现继承不同.在 public 继承下,derived class 总是继承base class 的接口.
pure virtual 函数只具体指定接口继承.
简朴的(非纯)impure virtual 函数具体制定接口继承以及缺省实现继承.
non-virtual 函数具体制定接口继承以及强制性实现继承.