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

In the previous article Breaking the Access Control Mechanism of C++ Classes, we briefly mentioned that the member access control of C++ classes (public/protected/private) only limits the accessibility of member names, rather than their visibility. This article mainly analyzes the consequences of this property and how to avoid them.

Accessibility vs. Visibility

First, let’s look at the definition given in the C++ standard:

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.

Consider the following example:

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;
// Which func is being called?
example.func(11.11);
}

However, the above code fails to compile with the compilation error:

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

Why isn’t our func call matched and implicitly converted to A::func(double)? Because the overload resolution happens before the accessibility check. When the compiler resolves a function call, three main things occur:

  • Name lookup. Before doing anything else, the compiler first looks for a scope containing at least one entity with the name of the calling entity and creates a list of candidate entities.
  • Overload resolution. Next, the compiler performs overload resolution to select the unique best match from the candidate overloaded functions.
  • Accessibility check. Finally, the compiler conducts an accessibility check to determine whether the selected function can actually be called.

We successfully passed name lookup and overload resolution with example.func(11.11);, but an error occurred during the accessibility check on the name selected in the first two steps! During overload resolution, the optimal candidate function matched for the call of example.func(11.11); is A::func(double), and when checking accessibility, it is found to be a private member of class A, leading to the compilation error.

Regarding the description of visibility and accessibility in C++’s design:

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?
};

The following is excerpted from Bjarne Stroustrup, the father of C++, in his book “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.
If public/private controls visibility rather than accessibility, changing public to private could silently change the meaning of a program, from one legal interpretation to another. Although I no longer regard this argument as conclusive (if I ever did), the decision made has proven useful as it allows programmers to add or remove public and private access indicators during debugging without altering the meaning of programs. I wonder if this aspect of the C++ definition is the result of a genuine design decision.

Since access control is about accessibility rather than visibility, any private members of a class are visible to any code that can see the class’s implementation, meaning they will participate in name lookup and overload resolution, which is the reason for the compilation error in the above example.

Avoiding Private Members from Name Lookup and Overload Resolution

Given the aforementioned issues with C++ class access control, how can we avoid them? One can encapsulate private members within another structure to prevent private members from participating in name lookup and overload resolution, which is known as the pimpl idiom.
Using the previously defined class A as an example, we will encapsulate its private members in another class:

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)
}

By wrapping a layer within the class and placing all private members in a location that won’t be accessed by name lookup and overload resolution, we can avoid the ambiguity caused by the C++ access control, which only restricts accessibility rather than visibility. However, the benefits of using the pimpl idiom extend beyond simply hiding class data and implementation at a deeper level. This will be the topic of my next article.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:访问控制机制的可见性与可访问性
Author:LIPENGZHA
Publish Date:2017/05/13 10:44
World Count:3.7k Words
Link:https://en.imzlp.com/posts/17586/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!