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,并判断何时不需要继承
Alternatives to virtual functions先定位 variation timing,再决定 dispatch、state 与 ownershipGameCharacterhealth calculation 的 contractNVI idiomsubtype variationpublic wrapper → hook固定 invariant envelopefunction pointerobject constructionfixed signature无状态 · 无 ownershipstd::functioncallable type erasurelambda / functor / capture验证空值与 lifetimeStrategy patternruntime replacementstate / resource / cache独立 ownership policy选择检查:变化属于 subtype、object、callable,还是独立算法对象?不要把“可替换”自动等同于“必须 public virtual”每条路径都要同时说明 contract、dispatch、state、ownership 与测试边界
同一项算法变化可以落在 subtype、object、callable 或独立 Strategy object;决策关键是变化时机与生命周期,而不是 virtual 关键字本身。

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 都要自己处理范围、锁、审计和错误。

Item 35 的原则是 Consider alternatives to virtual functions(考虑 virtual 函数替代方案)。替代不是“virtual 坏”,而是检查变化究竟属于对象 subtype、独立算法、运行策略还是固定流程的一步。

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。

derived 只返回 raw health,不会忘记 clamp 或 audit。

private virtual 也可以被 override

C++ access control 决定谁能调用 function,virtual overriding 决定谁提供实现;两者正交。derived 可 override base private virtual,尽管不能直接调用 base private function。

private hook 能阻止 derived 在任意时机直接调用,只允许 base algorithm 驱动;protected hook 则允许 derived 复用 base implementation。

选择 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。

函数指针简单、无 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。

这也让 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。

Result<void, Error> GameCharacter::setStrategy(
    std::unique_ptr<HealthStrategy> next);

先构造/验证 next,再锁内 no-throw swap;读取 health 时使用同一同步协议。

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。

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。

选择取决于变化发生在 build、object construction 还是 runtime。

几种方案的决策边界

  • 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

资料与写作方式声明

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

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

名词解释

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

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 的生命周期关系。

immutable shared strategy

多个 owners 共享的 const 算法对象。

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. 问题 1:把 public virtual healthValue 改成 NVI。 所有角色都必须验证、加锁、clamp 和审计,只有计算不同。
  1. 问题 2:选择函数指针或 std::function。 算法有时是普通函数,有时需要捕获难度系数和测试计数器,请设计。
  1. 问题 3:设计有状态毒素策略。 策略持有衰减参数和 cache,可在战斗中切换,多个角色不应串状态。

讨论

评论区加载中…