0%

cpp友元目的迷思

对C++中友元态度的迷思

老实说友元到底是不是封装的破坏者,这个问题我也是困扰过一段时间,后来想想其实抛开个人的不合理使用(都用C++了那不合理的使用就是改自己考虑清楚自己兜底),它并不应该被视作对封装的破坏。

最常见用途

最常见的用途其实表面上根本称不上是封装之类的思考,就是一个为类提供非成员接口。实际coding中,基本就是 operator<<保证流式输出,以及某些二元运算符重载,然后就是相对少见的单元测试的辅助类以及内部管理类(比如 Manager 访问 Worker 的私有状态等)。从这个视角看,单说语法,友元“破坏”封装;但为了支持某些必须放在类外的操作,friend 反而维持了封装的完整性

强化封装

但是最终我还是觉得友元不是破坏而是强化了封装。套个简单的例子,A类有一个私有变量 m,原本只有B类需要访问它。如果提供 getM/setM,反而让所有其他类都能看到这个接口,暴露了不该暴露的信息。此时把B设为友元,直接去掉公共接口,对外部其他类来说 m 就消失了,封装性更好。

从定义与原始目的上讲,封装的核心不是全部私有,而是最小暴露。当A只需要信任一个特定类B时,友元能够精确控制访问范围,避免产生本不需要的公共接口。空泛一点说,这在设计成对出现的类(如NodeList)、工厂模式、或者需要高性能访问内部表示的场景中很有价值。当然,前提是A和B确实由同一个人或同一模块维护,不然滥用友元确实会引入紧密耦合,导致一些,呃,不太妙的后果。

Dark Side?

模板加友元,确实可以访问任意类的private成员。滥用模板和友元可能绕过访问控制,甚至实现“黑客式”的单例破坏,不过这就是属于UB了。技术上,C++标准不允许通过模板特化随意访问任意类的私有成员,除非那个类主动声明了对应的模板函数/类为友元。这种瞎搞通常依赖于未定义行为(如指针偏移、aligned_storage等技巧)。实际coding中,这种写法既不合法也不可移植,属于是会被pre-commit或者CI打回的东西。只能说,友元的信任范围需要严格限定,如果滥用模板元编程和显式实例化,确实可能写出看起来能侵入私有成员的代码,但那不static,没有什么好说的。


概括

  1. 友元不是反封装工具,而是细粒度控制访问边界。批评友元破坏封装,往往是因为自己把它和将所有内部都暴露这种想法画等号,但真正破坏封装的是不加控制的 public 接口或把所有成员都弄成 protected,这属于前面的技术债或者菜狗的操作问题,和它无关。
  2. 友元的信任关系应该是对等/模块内的。如果两个类的耦合已经紧密到需要互相访问私有成员的地步了,显式声明友元比绕道公共接口更honest,也更利于后期维护。
  3. 滥用友元的问题在于让类之间的关系隐式扩散。如果一个类有十几个友元,那它几乎等于间接公开了所有私有成员,此时才是真正伤害了封装,但我觉得这不该怪友元,有一说一。
  4. 现代C++中,友元似乎确实越来越少,因为像Visitor这样的设计模式、private继承、Key模板等技巧可以替代部分友元场景。但在需要典型如流运算符的对称接口、单元测试桩、高性能容器实现时,友元仍然很好用。