第15条 访问权限的使用
谁能真正访问类的内部? 关于"伪造者", "骗子"和"偷窃者"以及如何分辨和避开它们;
1. 什么样的代码可以访问类的如下区段?
a) public
公用成员可以被任何代码访问;
b) protected
保护成员可以被类自身的成员函数访问, 也可以被类的友元访问, 可以被派生类的成员函数与友元访问;
c) private
私有成员只可以被类自身的成员函数以及类的友元访问;
这就是常规的答案, 而且本身无懈可击; 然而我们会考虑一个特例, 在特例中上述说法不再完整, 因为C++有时候提供了一种途径使得访问类的私有成员成为合法;
2. 考虑如下头文件;
1
2
3
4
5
6
7
8
9
|
// x.h
class
X {
public
:
X() : private_(1){}
template
<
class
T>
void
f(
const
T& t) {}
int
Value() {
return
private_;}
private
:
int
private_;
};
|
现在想让任何调用代码都能够直接诶访问到该类的private_成员, 分别给出符合以下条件的手法:
a) 一个非标准而且不可移植的伎俩;
b) 一个完全符合标准且可移植的技术;
人们对于一些破坏性的事情(诡秘的伎俩)仿佛有种奇怪的探知欲, 或许应当由一则"事故开始":
对于非标准且不可移植的伎俩, 有好几个方案; 下面是3个不太出名的"criminal suspect":
嫌犯#1: 伪造者
伪造者伎俩是先将某个有待伪造的类定义复制一份, 然后通过该复制后的"赝品"来达到想要达到的目的:
ex 15-1 谎言和伪造
1
2
3
4
5
6
7
8
9
|
class
X {
// this is not x.h, but a maually copied X's definition
// one line added:
friend
::Hijack(X&);
};
void
Hijack(X& x) {
x.private_ = 2;
// the evil win
}
|
[把奇怪的::Hijack改成void Hijack之后, gcc报错: redefinition]
这家伙是个伪造者, 不能相信; 当然, 伪造者的行为是非法的, 他违反了唯一定义规则ODR, 其中明确表示, Note 如果一个类型(X)被多次定义, 那么其所有定义都必须是完全相同的; 本例中被使用的对象或许可以称为X对象, 看起来像一个X对象, 然而它与程序其他地方使用的真正的X对象完全不是同一类型;
然而, 这种伎俩在很多编译器上可以工作, 因为通常真正的X类与伪造的X类的底层内存布局是一样的; 如果真是这样, 伪造者暂时还不会被抓住;
嫌犯#2: 偷窃者
偷窃者的伎俩是偷偷地改变类定义的含义:
ex 15-2: 可恶的宏伎俩
1
2
3
4
5
6
|
#define private public // illegal
#include "x_fake.h"
void
Hijack(X& x) {
x.private_ = 2;
// the evil won
}
|
偷窃者做的事情也是非法的; ex15-2不是可移植的:
- #define 保留字是非法的; [gcc编译通过]
- 这种伎俩和伪造者一样违反了单一定义规则ODR; 同样, 如果类的底层内存格局没有改变, 该手法或许还能暂时没事;
嫌犯#3: 骗子
骗子的伎俩在于"狸猫换太子"
ex.15-3 企图模拟原对象的内存布局
1
2
3
4
5
6
7
8
9
10
|
class
BaitAndSwitch {
// may have same memory layout as X
// so that cheat the compiler
public
:
int
notSoPrivate;
};
void
f(X& x) {
(
reinterpret_cast
<BaitAndSwitch>(x)).notSoPrivate = 2;
// the evil won
}
|
[gcc error: invalid cast from type 'X' to type 'BaitAndSwitch'; reinterpret_cast针对的是int类型(指针) 需要改成 (reinterpret_cast<BaitAndSwitch*>(&x))->notSoPrivate = 2;]
骗子的作为也是不合法的; ex15-3中的代码是非法的:
- X和BaitAndSwitch的内存布局并不保证是相同的, 尽管实际上他们或许总是相同的;
- reinterpret_cast的行为是未定义的, 尽管大多数编译器大多数都允许使用返回的结果引用, 正如"骗子"所期望的; 比较使用reinterpret_cast其实就是告诉编译器相信使用者, 撤销防备, 不再监视使用者下面将要做的非法勾当;
尽管许多犯罪伎俩是低劣, 不合法的, 然而也的确有些方法是遵从标准而且可接受的; 但我们还是想要寻找一个完全遵从标准的, 可移植的替代方案;
伪君子#4: 语言律师
ex15-4 合法的巧妙托词
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
namespace
{
struct
Y {};
}
template
<>
void
X::f(
const
T &t) {
private_ = 2;
// evil won
}
void
Test() {
X x;
cout<<x.Value()<<endl;
// print 1
x.f(Y());
cout<<x.Value()<<endl;
// print 2
}
|
[gcc error: 'T' does not name a type, 需要将特化版本f()中的T改成Y]
这家伙在钻语言的漏洞, 留神并避开这类卑鄙的家伙; 但是它实际上不是非法的; 因为它利用了X具有一个成员模板的事实; 其代码是完全合乎标准的, 标准也确保了这种代码会按照编码者的意图行事; 原因有两个方面:
- 针对任何类型来特化一个成员模板是合法的;
唯一可能会带来错误的举动是将它对同样的类型做了两次特化而且两次特化的方式又不同, 这样违反了唯一定义规则ODR, 本例没有这个问题:
- 因为这里的代码使用了一个确保具有唯一性的类型, 它位于匿名名字空间中; 因而语言确保了这种做法是合法的, 而且该特化也不会影响到其他任何人进行的任何特化;
不要搞破坏
3. 这是C++访问控制机制中的漏洞吗? 是C++封装机制中的漏洞吗?
例子展示了C++的两个语言特性(访问控制模型, 模板模型)之间的互相影响, 成员模板提供了一种有效的可移植的途径, 绕过类的访问控制机制, 从这个意义上来说似乎成员模板会隐式地"破坏封装性";
然而实际上这不是个问题, 问题在于, 是提防意外的误用(语言本身已经做的相当好了), 还是提防故意的滥用(要想做到几乎是不可能的); 最后, 如果程序员确实想要破坏类型系统, 他总能找到办法, 如ex15-1至15-3所示;
问题的真正答案是: 不要这么做; 有时候一个快速的, 暂时绕过访问控制机制的手段是诱人的, 例如在调试的时候能够给出更好的诊断输出, 然而对于产品代码来说这不是个好习惯, 应该把这条放在自己的警示列表中;
准则: 永远不要对语言搞破坏; e.g. 永远不要企图通过复制类定义在添加友元声明, 或提供成员模板函数特化等途径来破坏封装性;
---YC---