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
Encapsulation by access minimization先问“需要什么权限”,再决定函数放在哪里WebBrowser private representationcache · history · cookies · invariantsmember / friendprivileged access setpublic contractconvenience functionsrepresentation changesmember / friend 需要复查访问权限越宽,传播面越大non-member non-friend 通常不变封装不是“少写 member”,而是减少依赖 private 表示的代码数量
privileged access set 越小,private representation 替换时需要联动检查的函数越少。

从 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(优先以非成员非友元函数替换成员函数)。这里的“优先”针对不需要特殊权限的功能,不是取消所有成员函数。

封装取决于谁能看见 private 表示

encapsulation(封装)可以用一个具体问题衡量:当 private representation 改变时,有多少函数可能需要检查和修改?member 与 friend 都能访问全部 private data,因此都属于 privileged set。

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 一起变化。

这不是为了追求最少 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 可以按使用场景拆分。

browser/web_browser.hpp
browser/privacy.hpp
browser/bookmarks.hpp
browser/diagnostics.hpp

privacy 用户无需依赖 bookmarks parser;核心 class 用户无需为 diagnostics 引入日志后端。

这种组织也允许不同团队维护功能包,只共享稳定 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。

何时必须是 member

有些操作确实需要 member。constructor、destructor、assignment operator 受语言规则约束;virtual function 必须参与 class dispatch;需要原子维护 private invariant 的操作应由 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。

virtual audit 需要 dynamic dispatch,也必须是 member。不要为了减少 member 而把不变量拆成危险 public setters。

何时 friend 合理

friend 是精确授权工具,不是默认便利机制。某些 tight coupling、stream operator、serializer 或测试支撑在无法用 public API 高效正确实现时可能需要 friend。

使用 friend 前应回答:是否缺少一个合理 public semantic operation;授权者是否与 class 同一维护单元;representation 变化是否会共同审查。

若 friend 只是为了少写三个 public calls,它没有成立理由。

Function placement matrix:先检查 private 权限、virtual dispatch 和参数对称性
候选函数需要 private?需要 virtual?默认放置
clearEverything同 namespace non-member non-friend
withdraw是:原子不变量member function
audit可能是:virtualmember function
operator*public API 足够即可否对称 non-member non-friend
点号语法只是调用形式;权限、核心契约和 dispatch 才是 function placement 的证据。

对称运算通常适合 non-member

二元运算的两个 operands 地位相同,而且可能需要两侧都参与 implicit conversion。此类 operator 常应是 non-member;Item 24 会进一步讨论转换规则。

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 才最小授权。

不要把调用语法当作归属证明

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 验证函数位置,而不是依赖点号偏好

资料与写作方式声明

本章以Effective C++, Third Edition, Item 23权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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. 问题 1:重构 WebBrowser。 clearEverythingexportPrivacyReport 都是 member,但只调用 public API,请重新组织。
  1. 问题 2:判断三个候选函数。 withdraw 修改余额/冻结/审计;format 只调用 getters;audit 需要 derived override,分别放在哪里?
  1. 问题 3:审查 friend serializer。 serializer 被授予 friend 只为读取四个已有 public queries,如何处理?

讨论

评论区加载中…