《Effective C++》

条款33:避免遮掩继承而来的名称

遮掩行为与作用域有关。例子如下:

1
2
3
4
5
6
int  x; //global变量
void  someFun()
{
     double  x; //local 变量
     std::cin >> x; //读一个新值赋予local变量x
}

这个读取数据的语句指涉的是local变量x,而不是global变量x,因为内层作用域的名称会遮掩外围作用域的名称。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
     ...
};

此例内含一组混含了public和private名称,以及一组成员变量和成员函数名称。这些成员函数包括pure virtual、impure virtual和non-virtual三种,这是为了强调我们谈的是名称,和其他无关。

假设derived class内的mf4的实现码如下:

1
2
3
4
5
6
void  Derived::mf4()
{
     ...
     mf2();
     ...
}

当编译器看到这里使用了mf2,必须估算它指涉什么东西。编译器的做法是查找各作用域,看看有没有某个名为mf2的申明式。首先查找local作用域(也就是mf4覆盖的作用域),在那儿没有找到任何东西名为mf2.于是查找其他外围作用域,也就是class Derived覆盖的作用域。还是没找到任何东西名为mf2,于是再往外围移动,本例为base class。在那儿编译器找到一个名为mf2的东西了,于是停止查找。如果Base内还是没有mf2,查找动作便继续下去,首先找内含Base的那个namespace的作用域,最后往global作用域找去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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();
     ...
};
 
Derived d;
int  x;
...
d.mf1(); //OK
d.mf1(x); //error!!! Derived::mf1遮掩了Base::mf1
d.md2(); //OK
d.mf3(); //OK
d.mf3(x); //Error!!! Derived::mf3遮掩了Base::mf3

这些行为背后的基本理由是为了防止你的程序库或应用框架内建立新的derived class 时附带从疏远的base class继承重载函数。不幸的是你通常会想继承重载函数。实际上如果你正在使用public 继承而又不继承那些重载函数,就是违反base和derived class之间的is-a关系,而is-a是public 继承的基石。因此你总会想要推翻C++对“继承而来的名称”的缺省遮掩行为。可以使用using声明表达式达成这一目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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; //让Base class内名为mf1和mf3的所有东西在Derived作用域都可见
     using  Base::mf3;
     
     virtual  void  mf1();
     void  mf3();
     void  mf4();
     ...
};
 
Derived d;
int  x;
...
d.mf1(); //OK
d.mf1(x); //OK,调用Base::mf1
d.md2(); //OK
d.mf3(); //OK
d.mf3(x); //OK,调用Base::mf3

也就是说如果你继承base class并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。

有时你并不想继承base class的所有函数,这是可以理解的。在public继承下,这绝对不可能发生。但是在private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场。一个简单的转交函数(forwarding functions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class  Base
{
public :
     virtual  void  mf1() = 0;
     virtual  void  mf1( int );
     ...
};
 
class  Derived :  private  Base
{
public :
     virtual  void  mf1() //转交函数暗自为inline
     {    Base::mf1(); }
     ...
};
 
Derived d;
int  x;
d.mf1(); //OK,调用的是Derived::mf1
d.mf1(x); //Error!Base::mf1()被遮掩

总结:

  1. derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

  2. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)。

2016-11-09 12:56:26