访问控制机制的可见性与可访问性

在上一篇文章突破C++类的访问控制机制中简略提到了C++类的成员访问控制(public/protected/private)只是限制了成员名字的可访问性(accessable)**而非可见性(visable)**。在这篇文章中主要分析这种性质带来的后果以及如何避免。

可访问性与可见性

先来看一下C++标准中给出的定义:

1
2
3
4
5
6
7
8
9
class A {
class B { };
public:
typedef B BB;
};
void f() {
A::BB x; // OK, typedef name A::BB is public
A::B y; // access error, A::B is private
}

**[ISO/IEC 14882:2014]**It should be noted that it is access to members and base classes that is controlled, not their visibility. Names of members are still visible, and implicit conversions to base classes are still considered, when those members and base classes are inaccessible.

考虑下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A{
public:
void func(int){
cout<<"A::func(int)"<<endl;
}
private:
void func(double){
cout<<"A::func(double)"<<endl;
}
}

int main(){
A example;
// 调用的是哪个func?
example.func(11.11);
}

然而上面的代码并不能编译通过,具有编译错误:

1
error: 'func' is a private member of 'A'

为什么我们的fucn调用不会被匹配并隐式转换参数到A::func(double)呢?因为重载决议发生在可访问性检查之前。
编译器在对一个函数进行调用决议时,主要发生了三件事:

  • 名字查找。在做其他任何事情之前,编译器会首先寻找一个至少包含一个名为调用名字的实体的作用域,并将其中的候选实体列表。
  • 重载决议。接下来编译器会进行重载决议,这一步的目的是在候选的重载函数中选出唯一的最佳匹配。
  • 可访问性检查。最后,编译器会进行可访问性检查,以确定被选出的函数到底是否可被调用。

我们使用example.func(11.11);成功地通过了名字查找和重载决议,但是在前两布中选出来的名字在执行可访问性检查时发生了错误!
因为在重载决议时会匹配到最优的候选函数,在类A中,对example.func(11.11);的调用匹配出来的最优的匹配函数就是A::func(double),然后将其执行可访问性检查时发现其是类A的private成员,所以发生了编译错误。

关于C++设计时对于访问控制的可见性与可访问性的描述:

1
2
3
4
5
6
7
8
int  a;  //  global  a
class X {
private:
int a; // member X::a
};
class XX : public X {
void f() { a = l ; } // which a?
};

以下内容摘自C++之父Bjarne Stroustrup的著作《The Design And Evolution Of C++》Section 2.10 。

Making public/private control visibility, rather than access, would have a change from public to private quietly change the meaning of the program from one legal interpretation (access X::a) to another (access the global a). I no longer consider this argument conclusive (if I ever did), but the decision made has proven useful in that it allows programmers to add and remove public and private specifications during debugging without quietly changing the meaning of programs. I do wonder if this aspect of the C++ definition is the result of a genuine design decision.
如果用public/private控制可见性而不是可访问性,那么用户将public改为private的举动就有可能悄无声息地改变了程序的含义,使之从一个合法解释变成了另一个合法解释。虽说我不再认为当初的看法是结论性的(如果我曾经那样认为的话),然而当时作出的决定后来被证明是有用的,因为它允许程序员在对代码查错的时候添加或移除public和private访问指示符而同时又不会改变程序的含义。我不知道这是否算得上是一个真正的设计决策的结果。

既然访问控制的是可访问性而不是可见性,那么类内任何私有成员在任何可以看到类实现的代码都是可见的,即他们会参加名字查找和重载决议,这也就是上面例子中会产生编译错误的原因。

避免私有成员参加名字查找和重载决议

既然C++类的访问控制具有上面提到的问题,那么如何避免呢?可以将私有成员封装为另一个结构,从而避免私有成员参与名字查找和重载解析,这也就是所谓的pimpl惯用法。
同样使用上面所定义的类A为例子,但是我们将其private封装为另一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <memory>

struct A{
A();
void func(int){ cout<<"A:func(int)"<<endl; }
private:
struct Apimpl;
std::unique_ptr<Apimpl> privateMem;
};
A::A():privateMem(std::make_unique<Apimpl>()){}

struct A::Apimpl{
void func(double){
cout<<"A::func(double)"<<endl;
}
};

int main()
{
A x;
x.func(11.11); // call A::func(int)
}

通过在类中又裹一层中间层将所有的私有成员放到不会被名字查找和重载解析访问到的地方,来避免C++访问控制只是限制可访问性而非可见性所带来的歧义问题。但是使用pimpl惯用法的好处不仅仅是更深层次地隐藏类内数据和实现,还有额外的好处,这个此处按下不表,我打算将其作为下篇文章的主题。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:访问控制机制的可见性与可访问性
文章作者:查利鹏
发布时间:2017年05月13日 10时44分
本文字数:本文一共有2k字
原始链接:https://imzlp.com/posts/17586/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!