34.避免遮蔽继承而来的名字

前言

 
本节内容与继承相关性不大,重点基本上在作用域(scope)方面。


名称遮蔽规则

 
在下述代码中,

1
2
3
4
5
int x;//global变量
void doSth(){
double x;//local变量
...
}

作用域形势如下图所示:
image_1cb6lbv3l58g189p1oi8ndo1kqh9.png-9.5kB
当编译器遭遇名称x时,它会首先在doSth的作用域内搜寻,如果找到就不再查找。
c++的name-hiding rules只做一件事:遮掩名称。至于名称所对应的类型,它并不关注。


继承体系下的名称遮蔽规则

 
我们都很清楚当一个derived class成员函数内refer to base class内的某物(成员函数,typedef,成员变量)时,编译器可以找出我们所refer to的东西,这是因为derived class继承了声明于base class内的所有东西。
实际上的运作方式是,derived class的作用域被嵌套在base class作用域内。它们的关系如下所示:

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

image_1cb6lluob1dis16fq1vg41bk11jdtm.png-18.3kB
可以看出类中有着复杂的类型,但名称遮蔽体系对类型什么的根本无感。
假设derived class中的mf4实现如下:
1
2
3
4
5
void Derived::mf4(){
...
mf2();
...
}

当编译器处理到mf2时,必须了解它refer to的是何种东西。于是它按照以下顺序一一搜寻:

  1. mf4所覆盖的local作用域
  2. Derived class作用域内
  3. base class中作用域
  4. 含有base的namespace
  5. global作用域

实例分析

 
考虑如下程序:

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

image_1cb6m4s0m1e5ai8b15s7o481l1013.png-17.8kB
因为名称遮蔽原则的作用,base class内部所有名为mf1与mf3的函数都被derived class内的mf1函数与mf3函数遮蔽。我们可以认为,此时base中的mf1与mf3不再被继承。(无论它们的参数如何,是否virtual)
这些行为背后的基本理由是为了防止在程序库或者应用框架内建立新derived class时附带地从疏远的base class继承重载函数。
不幸的是,如果我们不继承这些重载函数,那就违反了is-a原则。


解决方案

using语句

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 mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class derived:public base{
public:
using Base::mf1;//让bc中一切名为mf1,mf2的一切东西在dc作用域内可见
using Base::mf3;//且为public可见度
virtual void mf1();
void mf3();
void mf4();
};

image_1cb6mrf3l13m0ku2tog17va6dr1g.png-19.2kB
这种操作意味着如果继承了带有重载函数的base class,而我们又希望重定义或override其中的一部分,我们必须使用using语句引入它们,否则某些你希望继承的名称将会被覆盖。


转交函数

有时候我们并不想继承bc的所有函数,这可以理解,但在public继承中绝无可能。(同时这也是using放在dc的public部分的原因:bc的public名称在publicly dc中也应该是public)
但是在private继承体系中是可能并且有意义的。假如dc以private形式继承bc,而它只是想继承那个mf1函数的无参数版本,此时我们有了新的方案::转交函数(forwarding function)

1
2
3
4
5
6
7
8
9
10
11
12
13
class base{
public:
virtual void mf1()=0;
virtual void mf1(int);
}
class derived:private base{
public
virtual void mf1(){base::mf1();}//转交函数
};
...
Derived d;
d.mf1();
d.mf1(5);//error!Base::mf1()被遮蔽

至此,我们已经讲完了继承和名称遮掩的完整故事,但是一旦继承结合了templates,我们又将面对“继承名称被遮掩”的新形式。


总结

 

  1. dc内的名称会遮掩bc内的名称,在public继承体系中此行为等于作死。
  2. 为了让被遮蔽的名称重见天日,我们可以使用using语句或者转交函数。