Item 26:尽可能延后变量定义

对齐 Effective C++ 第三版 Item 26:把变量定义延后到具备有效初值且即将使用的位置,消除失败路径的构造析构和默认构造后赋值,并量化循环内外变量的成本与作用域取舍。

学习目标

  • 能解释变量定义隐含的 constructor/destructor cost,并识别异常早退路径中的无用生命周期
  • 能修改 default-construct-then-assign 为验证后 direct initialization,使对象从定义起满足业务语义
  • 能比较 loop variable 在循环内构造与循环外复用的 assignment cost、作用域和状态风险

从一个从未使用却被析构的字符串开始

密码加密函数先定义结果,再验证输入:

std::string encryptPassword(const std::string& password) {
    std::string encrypted;
 
    if (password.length() < minimumPasswordLength) {
        throw LogicError{"password too short"};
    }
 
    encrypted = password;
    encrypt(encrypted);
    return encrypted;
}

若密码无效,encrypted 从未参与业务,却已经执行 default constructor,并在 stack unwinding 时执行 destructor。

Item 26 的原则是 Postpone variable definitions as long as possible(尽可能延后变量定义)。这里的“定义”不是写一个名字,而是开始对象生命周期并支付构造责任。

定义变量就会产生构造析构成本

对 class type,变量定义通常调用 constructor;每个成功构造的自动对象在离开 scope 时都调用 destructor,即使函数提前 return 或抛异常。

成本可能包含 allocation、reference count、lock、registration、file handle 或日志。编译器能消除某些纯临时对象,却不能假设所有有副作用构造都可删。

先预测每个 guard clause 是否经过变量定义;只在真正需要对象的路径定义它。

分步1 / 3

第 1 步:找到第一次必要使用

把输入检查、对象定义和首次使用排成一条控制流;先问失败路径是否真的需要支付 constructor/destructor cost。

延后定义先消除失败路径上的无用生命周期,再用 direct initialization 消除 default-then-assign。

延后到验证通过之后

修复第一步是把定义移到所有不依赖该对象的检查之后:

std::string encryptPassword(const std::string& password) {
    if (password.length() < minimumPasswordLength) {
        throw LogicError{"password too short"};
    }
 
    std::string encrypted;
    encrypted = password;
    encrypt(encrypted);
    return encrypted;
}

异常路径不再构造 encrypted,但成功路径仍然 default construct 后 assignment。

这通常比直接以最终值构造多做工作,也让对象短暂处于不具业务意义的状态。

延后到可以直接初始化的位置

更完整的写法在定义时就给出初值:

std::string encryptPassword(const std::string& password) {
    if (password.length() < minimumPasswordLength) {
        throw LogicError{"password too short"};
    }
 
    std::string encrypted(password);
    encrypt(encrypted);
    return encrypted;
}

一次 copy construction 代替 default construction 加 copy assignment;对象从定义起就有正确业务值。

最小作用域降低状态推理成本

变量越早定义,后续可修改它的语句越多,读者需要跟踪的状态区间越长。延后定义也在改善正确性。

if (request.requiresSignature()) {
    const Signature signature = signer.sign(request.payload());
    request.attach(signature);
}

signature 不应提前到 if 外;否则无签名路径也构造,并让后续代码误以为它总有意义。

最小 scope 减少 accidental reuse、stale value 和错误 alias。

分支结果优先表达为值

不要为了在 if 后使用而先定义空对象,再在每个 branch assignment:

const Config config = [&]() -> Config {
    if (useCache()) {
        return loadCachedConfig();
    }
    return fetchConfig();
}();

也可提取 named factory,使错误模型更清楚。关键是每条成功路径产生完整 Config,而不是共享一个可变 placeholder。

若使用 optional 表达真正的“可能尚无值”,要确保 absence 是领域状态,而不是绕开作用域设计的工具。

RAII guard 不能延后到失去保护

“尽可能晚”受正确性约束。lock guard、transaction guard、temporary file owner 必须在需要保护的第一条操作之前建立。

validateKey(key);                 // 不访问 shared state
std::scoped_lock lock(mutex_);    // 保护从这里开始
auto it = cache_.find(key);

验证可以在 lock 前减少持锁时间;一旦访问 cache,guard 不能再晚。

循环变量有两种成本模型

原书特别比较 loop variable(循环变量)。方案 A 在循环外定义并每轮赋值:

Widget widget;
for (int i = 0; i < n; ++i) {
    widget = makeWidget(i);
    process(widget);
}

成本大致是 1 次 constructor、1 次 destructor 和 n 次 assignment。

方案 B 在循环内直接定义:

for (int i = 0; i < n; ++i) {
    Widget widget = makeWidget(i);
    process(widget);
}

成本大致是 n 次 constructor 和 n 次 destructor,没有对既存对象 assignment。

assignment cost 不一定低于重新构造

assignment 要先处理旧状态,再接收新状态;可能释放资源、保留 capacity、处理 self-alias 或维护 identity。constructor 从空 storage 建立对象,路径可能更简单。

若 Widget assignment 能复用大 buffer,循环外可能更快;若 assignment 比 constructor+destructor 更复杂,循环内更好。必须 benchmark 真实类型和数据分布。

默认优先循环内最小 scope,因为语义清楚;只有热点证据支持时才扩大 lifetime 复用。

循环外复用会携带跨迭代状态

对象在迭代间存活,意味着未被 assignment 覆盖的成员、cache、error flag 或 observer 也会延续。

for (...) {
    parser.reset(input); // reset 是否真的清除所有 diagnostics/cache?
    parser.parse();
}

若 reset contract 不完整,复用会产生顺序相关 bug。per-iteration construction 通过新对象天然隔离。

性能优化必须同时测试重复顺序、失败后下一轮和不同输入组合。

Lab

Item 26 生命周期证据实验

先预测每条路径的 constructor、assignment 和 destructor,再切换失败、成功与循环样本。

当前路径

先 guard,再创建 encrypted

invalid → throw → ctor=0 → dtor=0

判定

延后定义消除了 unused lifetime path。

当前样本:失败输入;保存控制流、生命周期事件、成本模型和复位后的状态证据。

延后昂贵的 cold-path 对象

日志字符串、诊断报告、回滚 snapshot 常只在错误路径需要,应在条件成立后构造。

if (!result) {
    Diagnostic report = buildDiagnostic(request, result.error());
    sink.write(report);
}

不要在 hot path 开头预建 report “以备使用”。这减少 allocation,也让成功路径代码更聚焦。

注意诊断所需数据是否在 later point 仍有效;需要 snapshot 时,在生命周期丢失前保存最小信息,而不是提前构造完整报告。

现代返回值优化不改变作用域原则

factory 按值返回时,copy elision/move 常消除传递成本:

const auto session = Session::connect(endpoint);

这让“在需要处直接初始化”更自然。无需提前 default construct 再让 factory 填 output parameter。

错误可用 exception 或 Result 表达,成功值仍在检查后一次初始化。

用构造轨迹和控制流矩阵验收

先预测每条 return/throw/branch 会执行哪些 lifecycle operations,再建立门禁:

  • instrumented type 记录 constructor、assignment、destructor 与 allocation 次数。
  • invalid input/early return 证明昂贵变量从未构造。
  • success path 证明 direct initialization 替代 default-then-assign。
  • branch tests 证明变量只存在于真实使用路径且没有 placeholder 状态。
  • RAII tests 验证所有受保护副作用都位于 guard lifetime 内。
  • loop benchmark 比较 n 次 ctor+dtor 与 n 次 assignment,包括 capacity reuse。
  • sequence tests 验证循环外复用无 cross-iteration leakage。
  • compiler optimization report 检查 value-returning factory 的 elision,但不把优化当正确性前提。

只有成本和 scope 同时缩小,延后定义才真正完成。

小结

  • variable definition 会开始对象 lifetime,失败或早退路径也承担 constructor/destructor cost
  • 先执行不依赖对象的 validation,再在参数齐备处 direct initialize
  • 避免 default-then-assign 和跨分支 placeholder,让对象从诞生起具有 meaningful state
  • RAII guard 延后到最晚安全边界,但不能越过它必须保护的首个副作用
  • loop 外复用是一次构造加 n 次 assignment,loop 内定义是 n 次构造析构,应按语义和实测选择
  • 最小 scope 减少跨迭代状态、误用窗口和 cold-path 无效工作

名词解释

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

object lifetime interval

对象构造完成到析构开始的存在区间。

postponed variable definition

推迟到初值齐备且即将使用时定义。

constructor destructor cost

对象构造释放及资源副作用的总成本。

unused lifetime path

首次有意义使用前就退出的对象路径。

preconstruction guard

昂贵构造前执行的失败检查。

default-then-assign pattern

默认构造后再赋实际初值的模式。

direct initialization

以最终输入直接开始对象生命周期。

meaningful initial state

构造后立即满足当前领域语义的状态。

lexical variable scope

变量定义到 block 结束的可见范围。

branch-local state

只在实际分支存在的局部状态。

branch-produced value

各分支直接产生完整结果值的结构。

placeholder object

等待后续路径填充的无完整语义对象。

protective RAII guard

覆盖资源或临界区并自动清理的对象。

correctness-constrained postponement

不得越过保护责任起点的延后边界。

cross-iteration reuse

循环外保留对象并每轮赋值复用。

per-iteration construction

每轮创建并销毁独立对象。

assignment cost

替换已有状态与旧资源的成本。

lifecycle cost benchmark

测量构造析构赋值分配的循环基准。

cross-iteration state leakage

前一轮残留状态影响下一轮。

iteration isolation

每轮独立 identity 带来的状态隔离。

cold path
低概率错误、诊断或降级路径。
hot path
高频且对延迟敏感的主路径。
return value elision

直接在目标 storage 构造返回结果。

value-returning factory

以返回值提供完整对象的创建函数。

variable lifetime trace

控制流与 lifecycle 事件的对应轨迹。

练习

  1. 问题 1:重构 encryptPassword。 写出 invalid/success 两条路径的 lifecycle 事件,并消除无用操作。
  1. 问题 2:选择循环变量位置。 Widget 构造会分配 buffer,assignment 可复用 capacity,但 reset 可能残留 diagnostics,请设计决策实验。
  1. 问题 3:审查 lock guard 延后。 函数先验证 key,再读取 shared cache、随后才建 lock,请给出最晚安全位置。

资料与写作方式声明

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

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

讨论

评论区加载中…