第33条:
防止隐藏继承的名字
莎士比亚对于“名字”有着独特的见解。“名字意味着什么?玫瑰不叫玫瑰,依然芬芳如故。”大师还写道:“倘若有人偷窃了我的好名声……事实上会让我变得一贫如洗。”让这两段至理名言引领我们去探究
C++
中继承的名字。
事实上,本节讨论的问题与继承并没有太大关系。它仅仅关系到作用域。我们都能读懂下面的代码:
int x; //
全局变量
void someFunc()
{
double x; //
局部变量
std::cin >> x; //
读一个新值赋给局部变量
x
}
为
x
赋值的语句是关于局部变量
x
的,而不是全局变量
x
,这是因为内部作用域隐藏了(“遮挡了”)外部作用域的名字。我们可以将这种域间状况用下图描述:
当编译器执行至
someFunc
的作用域内并且遇到名字
x
时,它将在局部作用域内查找,以便确认此处是否包含与
x
这个名字相关的操作。因为如果有的话,编译器就不会再去检查其它任何作用域了。在这上面的示例中,
someFunc
中的
x
是
double
类型的,全局变量
x
是
int
类型的,但是这无关紧要,
C++
的名字隐藏准则只会这样做:隐藏名字。无论与名字相关的类型是否一致。本例中,
double
类型的
x
隐藏了
int
类型的
x
。
引入继承。我们知道当我们在一个派生类的成员函数中企图引用基类的某些内容(比如成员函数、
typedef
、或者数据成员等等)时,编译器能够找出我们所引用的东西,因为派生类所继承的东西在基类中都做过声明。这里真正的工作方式实际上是:派生类的作用域嵌套在基类的作用域中。请看下面示例:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf4();
...
};
本示例中同时存在公共的、私有的名字,另外同时包含了数据成员和成员函数的名字。成员函数包括纯需函数、简单虚函数(非纯虚的)和非虚函数。这就是向大家强调,我们此处讨论的中心话题就是名字。示例中还可以添加进类型的名字,比如枚举类型、嵌套类以及预定义类型的名字。这里讨论的核心是:它们都是名字,而它们是为什么东西命名的并不重要。示例中使用了单一继承结构,然而一旦你了解了
C++
中单一继承的行为方式之后,多重继承的行为也就不难推断了。
假定继承类中
mf4
是这样实现的(部分内容):
void Derived::mf4()
{
...
mf2();
...
}
当编译器看到这个函数中使用了
mf2
这个名字,它就能够找到
mf2
的出处。编译器是这样做到的:它通过搜寻名字为
mf2
的那处声明所在的作用域。首先它在本地作用域(也就是
mf4
以内)查找,但是没有找到任何名字为
mf2
的声明。随后编译器搜寻当前包含域,也就是
Derived
类的作用域。仍然没有找到,于是又转向搜索上一层作用域,也就是基类。在这里编译器终于找到了名叫
mf2
的东西,于是搜索结束。如果
Base
类中依然没有
mf2
,那么搜索仍会继续,从包含
Base
的名字空间开始,到全局作用域为止。
虽然我刚刚描述的查找过程是精确的,但是其对于
C++
中名字查找机制的描述依然没有做到面面俱到。索性我们的目标并不是对名字查找机制刨根问底从而去编写一个编译器。我们的目标是避免恼人的意外发生,针对这一点,我们掌握的信息已经足够了。
请再次考虑上面的示例,这次我们做一些小的改动:为
mf1
和
mf3
个添加一个重载版本,并且在
Derived
中为
mf3
添加一个新版本。(第
36
条中将会做出解释,
Derived
中重载版本的
mf3
(一个继承的非墟函数)将会使这样的设计存在无法避免的潜在危险,但是在此问题的焦点是继承下名字的可见性,我们暂且忽略这一问题。)
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
...
};
这段代码的行为将会使每个乍看到它的
C++
程序员吃上一惊。由于基于作用域的名字隐藏机制并没有改变,因此基类中所有名叫
mf1
和
mf3
的函数都被派生类中的
mf1
和
mf3
所隐藏。从名字查找的角度看,
Base::mf1
和
Base::mf3
不再被
Derived
继承!
Derived d;
int x;
...
d.mf1(); //
工作正常,调用
Derived::mf1
d.mf1(x); //
错误!
Derived::mf1
隐藏了
Base::mf1
d.mf2(); //
工作正常,调用
Base::mf2
d.mf3(); //
工作正常,调用
Derived::mf3
d.mf3(x); //
错误!
Derived::mf3
隐藏了
Base::mf3
就像你所看到的,即使同一函数在基类和派生类中的参数表不同,基类中该函数依然会被隐藏,而且这一结论不会因函数是否为虚函数而改变。类似地,在本条目最开端的实例中,
someFunc
中的
double x
隐藏了全局域中的
int x
,在此,
Derived
类中的函数
mf3
将
Base
类中名叫
mf3
但类型不同的函数隐藏起来。
C++
这一特性的理论基础是:可以防止一类继承意外的发生,那就是当你为一个库或应用框架创建一个新的派生类时,你可能会去继承远族基类中的重载版本。遗憾的是,我们通常情况下恰恰希望这么做。事实上,如果你使用公有继承,但不继承重载的元素,那么就有悖于公有继承的一项基本原则——基类和派生类之间是“
Derived
是一个
Base
”关系(见第
32
条)。既然如此,你就需要时时刻刻重载
C++
默认情况下隐藏的继承而来的名字。
这一工作通过使用
using
声明来实现:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
using Base::mf1;
//
让积累中所有名为
mf1
和
mf3
的东西
using Base::mf3;
//
在
Derived
的作用域中可见(并且是公有的)
virtual void mf1();
void mf3();
void mf4();
...
};
现在,继承将按部就班进行:
Derived d;
int x;
...
d.mf1(); //
依然正常,依然调用
Derived::mf1
d.mf1(x); //
现在可以了,调用了
Base::mf1
d.mf2(); //
依然正常,依然调用
Derived::mf1
d.mf3(); //
正常,调用
Derived::mf3
d.mf3(x); //
现在可以了,调用了
Base::mf3
这意味着如果你继承一个包含重载函数的基类,并且你仅期望对其中一部分进行重定义或重载,你就应该为每一个不期望被隐藏的名字添加一条
using
声明。如果你不这样做,一些你希望继承下来的名字将可能被隐藏。
不难想象,某些场合你可能不需要把基类中所有的函数继承下来。在公有继承体系下这是无论如何不可行的,在次声明,这是违背公有继承“
Derived
是一个
Base
”关系的。(这也是为什么上文中
using
声明要置于派生类中的公共元素部分:基类中公有的名字在公共派生类中必须是公有的。)然而在私有继承体系下(参见第
39
条),这种不完全继承依然是有意义的。比如,假设
Derived
类私有地继承自
Base
,并且
Derived
只希望继承
mf1
不包含参数的那个版本。
using
声明在此就不会奏效了,因为它将使该名字所代表的所有继承版本的函数在派生类中可见。在这种情况下可以使用另一种技术,我们称之为“转发函数”:
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
... //
照旧
};
class Derived: private Base {
public:
virtual void mf1() //
转发函数
;
{ Base::mf1(); } //
隐式内联(参见第
30
条)
...
};
...
Derived d;
int x;
d.mf1(); //
正常,调用
Derived::mf1
d.mf1(x); //
错误!
Base::mf1()
被隐藏了
内联转发函数的另一个用途是:在使用古老的编译器时,它们通常不支持使用
using
声明来为继承类的作用域引入继承的名字(这实际上是编译器的缺陷)。此时可以使用内联转发函数。
你已经了解了继承和名字隐藏的方方面面,但是当继承与模板同时使用时,又会出现“继承名字是隐藏的”一种全新的形式。
第
43
条将另起一行进行介绍。
铭记在心
l
派生类中的名字会将基类中的名字隐藏起来。在公有继承体系下,这是我们所不希望见到的。
l
为了让被隐藏名字再次可见,可以使
用
using
声明或者
转发函数。