Loading... 在考虑一个经典面试题——“如何在类外访问类的私有成员”的时候,群友在网上发现了如下一个做法: ### 通过显式实例化对access control打洞 ```cpp #include <cassert> #include <iostream> class A { public: int X() { return x_; } private: int x_; }; int A::*FiledPtr(); //返回A的int成员指针的函数声明 template <int A::*M> //这里A是非待决的一个限定,该模板形参是A成员指针 struct Rob { friend int A::*FiledPtr() { return M; } }; template struct Rob<&A::x_>; int main() { A o; o.*FiledPtr() = 10; assert(o.X() == 10); } ``` 这个方法是通过模板的**显式实例化**(Explicit Instantiation)实现的,因为在显式实例化的规则中有这样一个特例: > The **usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization**, with the exception of names appearing in a function body, default argument, base-clause, member-specifification, enumerator-list, or static data member or variable template initializer.[^1] 也就是说,当我们去做显式实例化的时候,实际上是可以合法访问某个class的private成员的。由此就催生了如上面代码所示的,对类的访问控制机制的打洞。按常理来说,这是一个巨大的隐患——因为我们可以合法的绕过访问控制了,而一般来说这种情况都是UB的[^2]。 那么,是什么需求使得标准不得不给予显式实例化以特权,哪怕是以给access control留下后门为代价呢? 请看下面的[例子](https://stackoverflow.com/a/23922329/19247642): ### 需求——导出显式实例化 ```cpp class Foo { private: struct Bar; template<typename T> class Baz { }; public: void f(); // does things with Baz<Bar> }; // explicit instantiation declaration extern template class Foo::Baz<Foo::Bar>; ``` 这是个很好的例子,给了我们对于需求的直观感受:当你需要导出一个显式实例化模板,而这个模板实例化涉及到私有成员的时候,访问控制和实例化的需求就产生了一个必然的冲突。而由于显式实例化是一个确实存在的现实需求,C++的做法从来就**不是为了维护其他规则而抹杀实现合理需求的可能性**。 如果真的要去通过这样的方法对access control打洞,那么可能产生的后果应该由程序员自己来掌控。 ### 大佬的评价(翻译) Johannes Schaub是[这样](https://stackoverflow.com/a/1044437/19247642)说的(甚至还给出了另外的方法): > 但实际上,这并不表明c++的访问规则不可靠。语言规则的设计是为了防止意外的错误——如果你试图抢夺一个对象的数据,语言的设计不会采取很长的方式来阻止你。 而著名的GotW中也[讨论](http://www.gotw.ca/gotw/076.htm)过这个问题,给出了这样的见解: > 以上给出了两个C++特性之间的有趣互动。访问控制模型和模板模型。事实证明,成员模板似乎隐含了 "破坏封装"的意思,因为它们有效地提供了一种可移植的方式来绕过类的访问控制机制。 > > 这实际上并不是一个问题。这里的问题是保护Murphy还是保护Machiavelli......也就是说,**保护意外的滥用**(该语言做得很好)与**保护故意的滥用**(这实际上是不可能的)。最后,如果一个程序员非常想颠覆这个系统,他就会找到一个方法,正如上面的例子1到3所证明的。 > > 这个问题的真正答案是。**不要这样做**!无可否认,甚至Scott Meyers也公开说过,有些时候,我们很想有一种快速的方法可以暂时绕过访问控制机制,比如为了在调试时产生更好的诊断输出......但这不该是你在编写代码时养成的习惯,而且它应该出现在你的开发环境的"警告行为"列表中。 > > 来自GotW的编码标准忠告: > > 永远不要颠覆语言;例如,永远不要试图通过复制一个类的定义并添加一个友元声明来打破封装,或者提供一个模板成员函数的本地实例(GotW #76) 总结来说,**通过技巧来故意绕过某些控制永远都是可能的,标准和语法检查想要阻止的不是这样的行为,而是无意识的错误。** [^2]: 大多数时候,其原因是:不同访问控制级别的非静态成员存储顺序是不确定的。手动操作指针偏移一定是不具有普适性的。 [^1]: ISO/IEC-14882:2020-13.9.1.6 © 允许规范转载 打赏 赞赏作者 赞 如果觉得我的文章对你有用,请随意赞赏
2 条评论
留个联系方式吧, 想成为你的群友之一
您好 我的交流群是786115408~