Item 35:考虑 virtual 函数以外的其他选择
对齐 Effective C++ 第三版 Item 35:以 GameCharacter health calculation 比较 NVI、函数指针、std::function 和 Strategy pattern,分析算法注入、状态、ownership、测试、同步及运行和编译期替换。
学习目标
- 能实现 NVI idiom,以 non-virtual public wrapper 固定前后条件并调用 virtual hook
- 能比较 function pointer 与 std::function 的 callable 能力、每对象选择、测试和运行成本
- 能设计 Strategy object 的接口、ownership、运行切换与 contract tests,并判断何时不需要继承
variation decision lab
先预测:这项变化应该放在哪里?
先判断变化时机、状态和 owner,再查看每种 alternatives to virtual functions 的验证证据;最后注入一个边界风险。
变化时机 · subtype variation
所有角色都必须经过同一个 public envelope;只有 health calculation 这一步随 subtype 变化。
设计判断
用 non-virtual wrapper 固定 validate、lock、clamp 和 audit,把 virtual implementation hook 收到 private/protected。
当前机制 · NVI idiom
用 Base reference 和 Derived object 调同一个 public entry,逐项记录 wrapper 前后条件;任何 override 都不能绕过 envelope。
从 healthValue 是否必须是 public virtual 开始
角色生命值可能因角色类型、装备、中毒状态或游戏模式而采用不同算法。直觉设计把差异放进 virtual:
class GameCharacter {
public:
virtual int healthValue() const;
};derived override 能工作,但 public virtual 同时把客户接口与 customization point 绑在一起。所有 override 都要自己处理范围、锁、审计和错误。
↡客户直接调用、derived 通过 override 替换实现的公开虚函数。Item 35 的原则是 Consider alternatives to virtual functions(考虑 virtual 函数替代方案)。替代不是“virtual 坏”,而是检查变化究竟属于对象 subtype、独立算法、运行策略还是固定流程的一步。
↡在 inheritance override 之外表达可替换行为的 interface、callable 或组合机制。NVI 把接口与 hook 分开
Non-Virtual Interface(NVI)使用 public non-virtual wrapper 调 private/protected virtual hook:
class GameCharacter {
public:
int healthValue() const {
std::scoped_lock lock(mutex_);
validateState();
const int raw = doHealthValue();
const int normalized = std::clamp(raw, 0, maxHealth());
auditHealth(normalized);
return normalized;
}
private:
virtual int doHealthValue() const = 0;
};NVI 仍使用 virtual dispatch,但替代了“直接公开 virtual”的接口设计。
NVI 集中不变量与横切逻辑
base wrapper 可统一执行 precondition、postcondition、lock、metrics、tracing 和 error translation。
↡跨所有 implementations 都必须一致执行的 validation、同步、审计与观测逻辑。derived 只返回 raw health,不会忘记 clamp 或 audit。
↡base 在 hook 前后执行并对全部 derived 生效的条件检查。private virtual 也可以被 override
C++ access control 决定谁能调用 function,virtual overriding 决定谁提供实现;两者正交。derived 可 override base private virtual,尽管不能直接调用 base private function。
↡访问权限控制调用表达式,virtual 机制独立决定 dispatch target 的关系。private hook 能阻止 derived 在任意时机直接调用,只允许 base algorithm 驱动;protected hook 则允许 derived 复用 base implementation。
↡base 独占 hook orchestration、derived 只提供 body 的 customization boundary。选择 private/protected 取决于 derived 是否需要显式调用共享 helper,而不是“virtual 必须 protected”。
函数指针把算法从 class hierarchy 移出
如果 health algorithm 不定义角色 subtype,可在每个 object 注入 function pointer:
class GameCharacter;
using HealthCalcFunction = int (*)(const GameCharacter&);
int defaultHealth(const GameCharacter& character);
class GameCharacter {
public:
explicit GameCharacter(
HealthCalcFunction calculator = defaultHealth)
: calculator_(calculator) {}
int healthValue() const {
return calculator_(*this);
}
private:
HealthCalcFunction calculator_;
};同一 class 的两个 objects 可选不同 calculator,无需创建 derived types。
↡行为在 object construction/configuration 时选择,而不是由 dynamic class type 决定。函数指针简单、无 ownership,但不能直接携带捕获状态,签名严格固定。
std::function 接受更广 callable
现代 std::function(原书时代的 tr1::function)可 type-erase function、lambda、functor 和 bind expression:
using HealthCalculator =
std::function<int(const GameCharacter&)>;
class GameCharacter {
public:
explicit GameCharacter(HealthCalculator calculator)
: calculator_(std::move(calculator)) {}
private:
HealthCalculator calculator_;
};GameCharacter poisoned{
[factor = 0.6](const GameCharacter& value) {
return static_cast<int>(value.baseHealth() * factor);
}
};std::function 可能进行 heap allocation(取决于 small-object optimization)并增加间接调用;空 callable 调用会抛 bad_function_call,constructor 应验证。
callable 访问 private state 的边界
外部 function/lambda 不应为了计算 health 成为 broad friend。给它一个稳定 public snapshot/context:
struct HealthContext {
int baseHealth;
int armor;
StatusEffects effects;
};
using HealthCalculator = std::function<int(const HealthContext&)>;GameCharacter 在锁内生成 context,再在适当同步策略下调用 calculator。
↡算法只依赖公开稳定 context,而不直接读取 owner private fields。这也让 calculator 可以独立 unit test。
Strategy object 适合有状态算法
经典 Strategy pattern 为算法建立独立 interface hierarchy:
class HealthStrategy {
public:
virtual ~HealthStrategy() = default;
virtual int calculate(const HealthContext& context) const = 0;
};
class GameCharacter {
public:
explicit GameCharacter(std::unique_ptr<HealthStrategy> strategy)
: strategy_(std::move(strategy)) {}
private:
std::unique_ptr<HealthStrategy> strategy_;
};Strategy 内部仍可 virtual,但变化层次与 GameCharacter hierarchy 分离。
ownership 决定策略生命周期
unique owner 表示每个 character 独占 mutable strategy;shared_ptr<const Strategy> 表示多个 characters 共享 immutable policy;reference 表示外部保证 lifetime。
std::shared_ptr<const HealthStrategy> sharedRules;不要默认 shared_ptr;若 strategy 有 per-character counters,共享会交叉污染状态并需要同步。
运行期切换需要事务语义
装备或游戏模式改变时可能替换 calculator/strategy。切换本身要定义 validation、thread safety 和 failure guarantee。
↡对象存活期间以新算法替换当前 strategy 的 operation。Result<void, Error> GameCharacter::setStrategy(
std::unique_ptr<HealthStrategy> next);先构造/验证 next,再锁内 no-throw swap;读取 health 时使用同一同步协议。
↡准备完整新策略后以不可失败 owner swap 发布,失败保留旧策略。callback 不应在持锁状态反向调用 character 并死锁,可通过 context snapshot 后解锁调用,或规定 non-reentrant contract。
测试替身不再需要伪造 subtype
callable injection 可直接注入固定边界值、异常和计数器:
int calls = 0;
GameCharacter testCharacter{
[&calls](const HealthContext&) {
++calls;
return 37;
}
};Strategy object 也可跑统一 contract suite,验证返回范围、determinism 和 failure behavior。
↡对所有 calculator implementations 重复执行的输入、输出与错误行为测试。GameCharacter tests 则验证 delegation、normalization、ownership 和 replacement。
compile-time policy 是另一种替代
若算法在 compile time 确定且不需要 heterogenous runtime objects,可用 template policy:
template<class HealthPolicy>
class BasicGameCharacter {
public:
int healthValue() const {
return policy_.calculate(context());
}
private:
[[no_unique_address]] HealthPolicy policy_;
};代价是每个 policy 产生不同 static type、可能 code bloat、实现需 header 可见,不能轻易 runtime replace。
↡编译期 specialization 用更多类型和实例化换取静态分派优化的权衡。选择取决于变化发生在 build、object construction 还是 runtime。
几种方案的决策边界
↡按变化轴、contract envelope、state、ownership、dispatch 和测试选择机制的表。- NVI:variation 与 subtype 一致,但 public contract/横切流程必须固定。
- function pointer:无状态、固定 signature、低复杂度函数选择。
std::function:需要 lambda/functor/captured state 与按对象注入。- Strategy object:算法有独立 interface、状态、资源和生命周期。
- template policy:build-time 固定、性能敏感且接受 static types/code generation。
不要因为熟悉 inheritance 就把所有 variation 变成 derived class。
用调用轨迹和替换矩阵验收
先预测每种方案中 public contract、算法选择时机、owner 和 dispatch target,再验证:
- NVI hook 成功/失败/异常都经过 validate、clamp、audit 和 lock policy。
- function pointer 默认值、null rejection 与不同对象不同算法。
std::function覆盖 free function、capturing lambda、functor、empty callable 和 allocation profile。- context snapshot 不泄漏 private handles,callback lifetime 无悬空 capture。
- Strategy unique/shared ownership 覆盖 copy/move/destruction 与 shared mutable rejection。
- runtime replacement 注入构造失败、并发读取、reentrancy 和 cancellation。
- strategy contract suite 对所有 implementations 重跑。
- template policy 比较 binary size、compile time 与 runtime profile。
机制选择需要同时通过行为正确性和生命周期证明,而不只是“能调用”。
小结
- NVI 以 non-virtual public wrapper 固定 contract,private/protected virtual hook 定制算法
- function pointer 适合无状态固定签名算法,并允许同 class objects 使用不同函数
- std::function/tr1 function 支持 lambda、functor 和 captured state,需管理空值、分配和 lifetime
- Strategy pattern 把算法 interface、状态与 ownership 独立于 GameCharacter hierarchy
- runtime replacement 要 prepare/swap 并定义同步、reentrancy 与 failure guarantee
- compile-time policy 以 static type/code generation 换取内联和零 type-erasure dispatch
名词解释
本章出现的专业名词,用大白话再讲一遍。
- public virtual customization
客户直接调用且 derived override 的 virtual。
- behavioral variation mechanism
表达可替换行为的 inheritance/callable/strategy。
- NVI idiom
public nonvirtual wrapper 调 virtual hook。
- virtual implementation hook
只负责可替换算法步骤的 virtual。
- cross-cutting contract logic
所有实现共用的验证同步审计。
- invariant envelope
hook 前后由 base 固定执行的条件。
- access-dispatch separation
访问权限与 virtual overriding 相互独立。
- base-controlled dispatch
base 决定 hook 调用时机的边界。
- function pointer strategy
以函数地址注入固定签名算法。
- per-object algorithm selection
按对象而非 subtype 选择行为。
- callable type erasure
统一保存不同 concrete callable types。
- captured callable state
lambda/functor 随调用携带的环境状态。
- small callable optimization
小 callable 内置存储避免 heap。
- strategy input context
算法所需的稳定 value snapshot。
- strategy encapsulation boundary
策略不依赖 owner private representation。
- strategy pattern
算法封装为独立 interface object。
- stateful strategy object
拥有参数缓存资源的算法对象。
- strategy ownership policy
character 与 strategy 的生命周期关系。
- runtime strategy replacement
对象存活期间替换算法。
- strategy replacement guarantee
候选验证后 no-throw 发布的保证。
- algorithm test double
测试注入的可控 callable/strategy。
- strategy contract suite
对所有算法实现重跑的行为测试。
- compile-time policy strategy
template parameter 组合算法。
- static polymorphism tradeoff
更多 types/instances 换取静态分派。
- behavior variation matrix
机制、state、owner 与 dispatch 决策表。
- variation timing axis
变化在 build、construction 或 runtime 发生。
- strategy dispatch trace
wrapper 到算法及返回的调用轨迹。
练习
- 问题 1:把 public virtual healthValue 改成 NVI。 所有角色都必须验证、加锁、clamp 和审计,只有计算不同。
- 问题 2:选择函数指针或 std::function。 算法有时是普通函数,有时需要捕获难度系数和测试计数器,请设计。
- 问题 3:设计有状态毒素策略。 策略持有衰减参数和 cache,可在战斗中切换,多个角色不应串状态。