Item 23:优先以非成员非友元函数替换成员函数
对齐 Effective C++ 第三版 Item 23:以 private 访问集合衡量封装,把只组合 public API 的便利功能设计为同命名空间的非成员非友元函数,并识别必须使用成员或友元的边界。
学习目标
- 能解释为什么只组合 public API 的 non-member non-friend function 比额外 member 更有利于封装
- 能设计与 class 同 namespace、按主题 header 拆分的 convenience functions
- 能判断 private invariant、virtual dispatch、语言规则和对称参数何时要求 member、friend 或 non-member
从 clearEverything 应该放在哪里开始
假设浏览器类型已经提供三个核心操作:
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
};产品还需要“一键清理全部数据”。最直接的写法是再加 member:
class WebBrowser {
public:
void clearEverything();
};但 clearEverything 只需依次调用三个 public functions,不需要 private data。把它放进 class,会无条件获得访问全部 private representation 的能力。
Item 23 的原则是 Prefer non-member non-friend functions to member functions(优先以非成员非友元函数替换成员函数)。这里的“优先”针对不需要特殊权限的功能,不是取消所有成员函数。
↡为常见用例组合多个核心 public operations、但不建立新底层能力的函数。封装取决于谁能看见 private 表示
encapsulation(封装)可以用一个具体问题衡量:当 private representation 改变时,有多少函数可能需要检查和修改?member 与 friend 都能访问全部 private data,因此都属于 privileged set。
↡所有能够直接读取或修改某个 class private representation 的函数和类型集合。void clearEverything(WebBrowser& browser) {
browser.clearCache();
browser.clearHistory();
browser.removeCookies();
}这个 free function 只能依赖 public contract。即使 WebBrowser 把 cache 从 map 换成 database,它也无需变化。
↡通过减少依赖内部表示的代码数量,降低实现变化的传播范围。member 数量也是长期承诺
public member 会进入 class interface、文档、自动补全、ABI 和用户认知。每增加一个 member,都暗示它是类型的核心能力,并可能与 inheritance、mock 和版本兼容绑定。
↡class definition 对用户承诺的核心操作、virtual surface 和二进制可见成员集合。便利组合通常可以独立增加、弃用和分包,不应强迫核心 class definition 一起变化。
↡不修改 class definition,仅通过现有 public contract 添加领域组合操作的能力。这不是为了追求最少 member,而是让核心能力与组合能力拥有不同演进节奏。
同一 namespace 保持逻辑归属
non-member 不等于“随便放一个全局函数”。应与 class 放在同一 namespace:
namespace browser {
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
};
void clearEverything(WebBrowser& value);
} // namespace browser调用形式 browser::clearEverything(web) 清楚表达领域归属。argument-dependent lookup 也能在适当泛型调用中发现同 namespace 函数。
不要把用户扩展放进 std;除标准明确允许的 specialization 外,向 std 添加声明是错误边界。
便利函数可以按主题拆分 header
class members 必须在 class definition 中声明,通常迫使所有用户看见全部功能。namespace convenience functions 可以按使用场景拆分。
↡把同一类型的可选组合功能分布到多个主题 header,用户只包含所需部分。browser/web_browser.hpp
browser/privacy.hpp
browser/bookmarks.hpp
browser/diagnostics.hppprivacy 用户无需依赖 bookmarks parser;核心 class 用户无需为 diagnostics 引入日志后端。
↡一个头文件修改后因 include 关系而必须重新解析和编译的下游范围。这种组织也允许不同团队维护功能包,只共享稳定 public contract。
可扩展性不应要求改 class
第三方无法给已有 class 添加 member,却能在自己的 namespace/module 中基于 public API 提供新算法。
↡用户在不取得内部权限、不修改原类型源码时增加组合行为的能力。namespace analytics {
Report summarizePrivacyState(const browser::WebBrowser& value);
} // namespace analytics若功能应成为 browser 官方生态,可在 browser namespace 的扩展 header 发布;若是应用特定策略,可留在应用 namespace。关键是都不扩大 private access。
↡扩展只消费稳定 public contract,因此 core representation 改变时仍可独立维护。何时必须是 member
有些操作确实需要 member。constructor、destructor、assignment operator 受语言规则约束;virtual function 必须参与 class dispatch;需要原子维护 private invariant 的操作应由 class 掌控。
↡由 C++ 语法规定只能或自然必须声明在 class 内部的操作。class Account {
public:
Result<void, Error> withdraw(Money amount);
virtual void audit(AuditSink&) const = 0;
Account& operator=(const Account& rhs);
};withdraw 同时检查余额、冻结状态和审计序号,若 public primitives 无法安全组合,它就是核心 member。
↡必须在单一受权操作中读取并提交多个 private 状态,才能维持对象不变量。virtual audit 需要 dynamic dispatch,也必须是 member。不要为了减少 member 而把不变量拆成危险 public setters。
何时 friend 合理
friend 是精确授权工具,不是默认便利机制。某些 tight coupling、stream operator、serializer 或测试支撑在无法用 public API 高效正确实现时可能需要 friend。
↡为一个窄职责函数或类型授予 private access,并把它纳入同一封装维护边界。使用 friend 前应回答:是否缺少一个合理 public semantic operation;授权者是否与 class 同一维护单元;representation 变化是否会共同审查。
↡class 与其必要 friend 作为一个整体维护和演进 private representation 的边界。若 friend 只是为了少写三个 public calls,它没有成立理由。
| 候选函数 | 需要 private? | 需要 virtual? | 默认放置 |
|---|---|---|---|
| clearEverything | 否 | 否 | 同 namespace non-member non-friend |
| withdraw | 是:原子不变量 | 否 | member function |
| audit | 可能 | 是:virtual | member function |
| operator* | public API 足够即可否 | 否 | 对称 non-member non-friend |
对称运算通常适合 non-member
二元运算的两个 operands 地位相同,而且可能需要两侧都参与 implicit conversion。此类 operator 常应是 non-member;Item 24 会进一步讨论转换规则。
↡二元操作的左右参数在领域语义中地位相同,不存在天然 receiver。class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
};
Rational operator*(const Rational& lhs, const Rational& rhs);这里是否 friend 取决于 public API 是否足以高效读取表示。优先 non-member non-friend;只有确需 private access 才最小授权。
↡函数参数列表完整显示所有参与者,不把一个 operand 隐藏为 this 的接口形式。不要把调用语法当作归属证明
object.doThing() 看起来“面向对象”,不代表 doThing 应该是 member。真正问题是它需要什么权限、是否维护 invariant、是否 virtual、是否属于核心 type contract。
现代 IDE 同样能发现 namespace functions;可以通过命名、文档分组和 module exports 提高可发现性,而不是牺牲封装。
用表示替换实验验证函数位置
函数放置实验
从权限证据决定 member 还是 non-member
先猜一猜:把同一个候选函数放进 class,会不会扩大封装的联动范围?再切换场景验证。
候选函数 · clearEverything
convenience function
推荐放置
同 namespace 的 non-member non-friend
应观察到
只调用 clearCache、clearHistory、removeCookies;private representation 替换时无需修改。
判断顺序:private invariant → virtual dispatch → symmetric operands → public contract。
先预测 WebBrowser private cache/history/cookies 全部更换表示时,哪些函数需要修改,再建立门禁:
- 静态清单统计 member、friend 与 non-member 的 private access 权限。
- representation swap 后 convenience function 源码和测试保持不变。
- include graph 验证 privacy/bookmarks/diagnostics 用户只引入所需依赖。
- compile-negative tests 证明 convenience functions 不能访问 private data。
- core invariant tests 验证必须为 member 的操作仍原子维护状态。
- ADL/namespace tests 验证泛型调用发现正确扩展且没有全局污染。
- API review 为每个新增 member 写出“语言要求、virtual 或 invariant-critical”理由。
如果一个候选 member 只调用 public API,它默认应移出 privileged set。
小结
- non-member non-friend function 只能依赖 public contract,因此通常比额外 member 更加强封装
- friend 与 member 都能访问 private representation,都会扩大 privileged access set
- convenience functions 应与 class 放在同 namespace,并按主题 header 拆分
- client-side extension 可以不修改 class definition、不增加核心编译依赖
- constructor、assignment、virtual 与 invariant-critical operation 仍应是 member
- 用 privilege-driven placement 和 representation swap 验证函数位置,而不是依赖点号偏好
名词解释
本章出现的专业名词,用大白话再讲一遍。
- non-member non-friend function
只使用公开契约、没有成员或友元权限的函数。
- convenience function
组合核心 public operations 的常用辅助功能。
- privileged access set
可直接访问 private 表示的代码集合。
- encapsulation by access minimization
通过减少表示依赖者增强封装。
- class interface surface
class definition 承诺的核心成员集合。
- open namespace extension
不改 class 即可增加 namespace 功能。
- namespace association
class 与相关 free functions 的逻辑命名空间归属。
- argument-dependent lookup
按参数类型 namespace 补充函数查找。
- convenience header partitioning
按主题 header 拆分可选组合功能。
- compile dependency surface
头文件变化影响的下游编译范围。
- client-side extension
用户仅基于 public API 增加行为。
- contract-only extension
只依赖稳定公开契约的扩展。
- language-required member
语言规则要求位于 class 内的操作。
- invariant-critical operation
必须原子维护 private 状态的操作。
- narrow friend grant
向窄职责协作者授予 private access。
- joint encapsulation unit
class 与必要 friend 的共同维护边界。
- symmetric operation
左右 operands 领域地位相同的操作。
- explicit operand symmetry
参数列表显式展示所有对称参与者。
- privilege-driven placement
按权限和契约决定函数位置。
- function placement matrix
函数权限、dispatch 与组织方式审查表。
练习
- 问题 1:重构 WebBrowser。
clearEverything与exportPrivacyReport都是 member,但只调用 public API,请重新组织。
- 问题 2:判断三个候选函数。 withdraw 修改余额/冻结/审计;format 只调用 getters;audit 需要 derived override,分别放在哪里?
- 问题 3:审查 friend serializer。 serializer 被授予 friend 只为读取四个已有 public queries,如何处理?