Item 37:绝不重新定义继承而来的缺省参数值

对齐 Effective C++ 第三版 Item 37:解释 virtual function body 动态绑定而 default parameter value 静态绑定,复现 Shape/Rectangle 默认颜色分裂,并用 NVI 或显式参数统一默认政策与发布版本。

学习目标

  • 能解释 default argument 在 call site 按静态类型补入、virtual body 按动态类型分派的两阶段过程
  • 能复现同一 Rectangle override 经 Shape pointer 与 Rectangle pointer 收到不同默认颜色
  • 能实现 NVI 或显式参数 API,统一默认政策并验证 header/library 版本和旧客户行为
Default value / virtual dispatch mapcall site first → virtual function body laterstatic viewShape* / Rectangle*可见 declarationdefault parameter valuestatically boundRed ≠ Greenvirtual functiondynamic body dispatchRectangle::drawredefine inherited default parameter → view-dependent inputBase client: RedConcrete client: GreenNVI: one defaulthook: explicit valuedefault 改动属于 caller/version 变更;不要把 header 替换误当 runtime policy 更新
默认值在调用点静态补入,virtual body 才动态分派;NVI 用唯一 public 入口消除默认政策分裂。

默认绑定实验

先预测:默认值在哪里决定?

先预测 call site 插入的值和最终 virtual body,再切换视图查看版本与 NVI 证据。

观察

Shape* 调用先从 Shape declaration 补入 Red,再把完整参数交给 virtual function;Rectangle* 则可能先补入 Green。

政策

把“参数默认值”和“body 动态分派”拆成两个阶段解释,不能因为 body 相同就认为参数政策相同。

当前政策 · default parameter value / virtual function

把“参数默认值”和“body 动态分派”拆成两个阶段解释,不能因为 body 相同就认为参数政策相同。

从同一个 override 收到两种颜色开始

class Shape {
public:
    enum class Color { Red, Green, Blue };
 
    virtual ~Shape() = default;
    virtual void draw(Color color = Color::Red) const = 0;
};
 
class Rectangle : public Shape {
public:
    void draw(Color color = Color::Green) const override;
};

Rectangle 重新声明 default 为 Green,看似表示自己的默认颜色。实际调用会混合两种绑定规则。

Item 37 的原则是 Never redefine an inherited default parameter value(绝不重定义继承缺省参数)。

virtual body 动态绑定,默认参数静态绑定

Rectangle rectangle;
 
Shape* shapeView = &rectangle;
Rectangle* rectangleView = &rectangle;
 
shapeView->draw();     // Rectangle::draw(Color::Red)
rectangleView->draw(); // Rectangle::draw(Color::Green)

两个 calls 最终都执行 Rectangle::draw,但第一个参数来自 Shape declaration,第二个来自 Rectangle declaration。

这比 Item 36 更隐蔽:body 一致,只有隐藏传入值不同。

compiler 在 call site 补写默认值

概念上,编译器把省略参数的调用改写:

shapeView->draw(Shape::Color::Red);
rectangleView->draw(Shape::Color::Green);

之后才生成 virtual dispatch。

C++ 选择静态绑定避免在每次 virtual call 额外查找动态 default metadata;语言没有为 vtable 携带默认值。

static type 改变就会改变默认值

reference、pointer、smart pointer dereference、template parameter 的 static type 都会影响可见 declaration。

void paintShape(const Shape& shape) {
    shape.draw(); // always inserts Shape::Red
}

即使实际传 Rectangle/Circle,paintShape body 已按 Shape declaration 编译。

这正是多态 API 的常态,因此 derived default 往往只影响少数 concrete calls,制造不一致。

override 不检查默认值一致性

override 检查函数 signature 是否覆盖,但 default argument 不属于 function type/signature,因此 Green 与 Red 不会触发 compile error。

void draw(Color color = Color::Green) const override; // 合法但危险

需要 lint/code review 规则禁止 override 重复 default。

即使默认值相同,也不要复制声明

Derived 写相同 Red 暂时行为一致,但复制了 policy;Base 改为 Blue 后 Derived 可能未同步。

class Rectangle : public Shape {
public:
    void draw(Color color) const override; // no default here
};

override 调用仍可从 Shape interface 省略参数,concrete Rectangle caller 若直接调用则必须显式给 color;这反而暴露了接口层次差异。

NVI 把默认政策放进唯一入口

更完整修复是 public non-virtual wrapper 携带 default,private virtual hook 不带 default:

class Shape {
public:
    void draw(Color color = Color::Red) const {
        validate(color);
        doDraw(color);
    }
 
private:
    virtual void doDraw(Color color) const = 0;
};
 
class Rectangle : public Shape {
private:
    void doDraw(Color color) const override;
};

NVI 也可集中 validation、lock、metrics 和 error mapping。

不需要默认时要求显式参数

颜色是重要业务选择时,最安全 API 是不提供 default:

virtual void draw(Color color) const = 0;

可在上层提供 named convenience:

void drawWithTheme(const Shape& shape, const Theme& theme) {
    shape.draw(theme.defaultShapeColor());
}

这适合 default 依 theme/user/config 变化的系统。

若 subtype 默认真的应该动态变化

有时“无参 draw”本身就是 polymorphic behavior。不要依赖 default argument;建两个 operations:

class Shape {
public:
    void draw() const {
        doDraw(defaultColor());
    }
 
    void draw(Color color) const {
        doDraw(color);
    }
 
private:
    virtual Color defaultColor() const = 0;
    virtual void doDraw(Color color) const = 0;
};

这增加 virtual calls,但语义真实;要验证 defaultColor 的 invariant 与成本。

default argument 不属于 function pointer type

通过 function pointer/callback 调用时,default 不随函数地址携带:

using DrawFunction = void (*)(Shape::Color);

调用 pointer 必须提供参数,除非当前调用语法有另一个可见 declaration 可用;callback registry 不能依赖原函数 default 自动传播。

adapter/lambda 应明确传入 policy。

修改 default 是 call-site 版本变更

header v1 为 Red,客户 binary 编译后 call site 已包含 Red。库 header 改 Blue 并只替换 shared library,旧客户仍传 Red。

安全/协议关键 default 改动需客户 rebuild、API version 或 runtime config,不应假设动态库热更足够。

默认表达式也可能有副作用

default argument 可以是函数调用:

void send(Message message, TraceId trace = nextTraceId());

求值发生在 call site,exception、thread context、source location 和 binary version 都属于 caller。

多态 override 重声明不同 side-effecting defaults 会让问题更严重,应禁止。

多声明默认值容易产生可见性差异

C++ 允许在规则约束下从同一 scope 的 declarations 累积不同参数 defaults,但同一参数不能在同一可见 scope 重定义。跨 header/namespace 的可见声明差异会让调用可用性不同。

公共 API 应在单一 canonical declaration 定义 defaults,其他声明不重复。

用静态视图和版本矩阵验收

先预测 call expression 静态类型、插入值与最终 virtual body,再验证:

  • 同一 Rectangle object 经 Shape*/Shape&/Rectangle*/Rectangle& 调无参 draw。
  • override declarations 不包含 default argument,lint 作为 error。
  • explicit color calls 在所有 views 得到相同 body/value。
  • NVI wrapper tests 验证 default 只插入一次,hook 永远接收显式值。
  • dynamic default overload tests 覆盖 subtype defaultColor 与显式 color。
  • function pointer/std::function callback 必须显式传参数。
  • header v1/v2 + old/new client + shared library 组合验证 caller-baked value。
  • side-effecting default 记录求值线程、异常和调用次数。

只有默认来源唯一且版本行为明确,polymorphic API 才可维护。

小结

  • virtual function body 动态绑定,default parameter value 静态绑定
  • compiler 先按 call expression static type 补入 default,再执行 virtual dispatch
  • derived 重声明不同 default 会让同一 override 经 Base/Derived views 收到不同值
  • override 不检查 default,因为 default 不属于 function signature
  • NVI 把 default 放在唯一 non-virtual wrapper,virtual hook 只接收显式参数
  • default 被编进 caller,修改 header 需要重编译或版本化,不能只替换 library

资料与写作方式声明

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

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

名词解释

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

default parameter value

调用省略参数时编译期提供的表达式。

inherited default redefinition

override 对同一参数声明不同 default。

dynamic function binding

运行期按动态类型选择 virtual body。

statically bound default

编译期按静态声明决定 default。

call-site default substitution

compiler 在 caller 补写省略实参。

declaration-side default policy

default 属于声明调用语法而非 vtable。

call expression static type

决定 default lookup 的编译期类型。

base-client default boundary

base client 总采用 base declaration default。

default-not-in-signature rule

default 不参与 override signature。

override default blind spot

override 无法检查 default 一致性。

duplicated default policy

多个 declarations 复制同一 default。

single-source default

默认只出现在一个 canonical 声明。

NVI default policy

nonvirtual wrapper 定义 default 和流程。

uniform default entry

全部静态 views 使用同一默认入口。

explicit argument contract

所有 callers 明确提供参数。

context-owned default

由调用上下文计算并传入 policy。

dynamic default operation

virtual operation 计算 subtype default。

overload-based default API

无参和显式参数 overload 分离。

default-free function type

function type 不编码 default argument。

callback default boundary

callback adapter 必须显式处理缺省值。

caller-baked default

编译进 caller machine code 的默认值。

default-version skew

旧新 callers 混用不同默认值。

evaluated default expression

省略参数时在 caller 求值的表达式。

caller-side default evaluation

默认副作用和异常发生在调用方。

default argument accumulation

多 declarations 分散提供 defaults。

declaration visibility skew

不同 TU 看到不同 default declarations。

default-binding matrix

静态视图、插值、body 与版本测试表。

练习

  1. 问题 1:redefine inherited default parameter 与 default parameter value。 Base default Red、override default Green,同一对象经两种 pointers 调 draw,写出两阶段绑定。
  1. 问题 2:statically bound default 与 virtual function。 每种 Shape 确实需要不同无参默认,但显式 draw(color) 仍共用接口。
  1. 问题 3:default parameter value 的 caller/version 发布变更。 header 从 Red 改 Blue,只替换 shared library,线上同时有旧新客户,分析并制定方案。

讨论

评论区加载中…