返回专辑
·Johan·5 分钟阅读

值类别基础:lvalue / rvalue 与表达式结果

值类别与移动:左值/右值/xvalue 怎么影响机器人 pipeline 接口。

值类别基础:lvalue / rvalue 与表达式结果

1. 接口没问题,却每帧 copy

profiling 显示 Cloud copy ctor 在 hot path——签名 void process(const Cloud& c),实现 cache_ = c 成员按值存储。根因是不懂值类别:表达式是 lvalue 还是 prvalue/xvalue,决定绑定到 T&、const T& 还是触发 move。pipeline 误 copy 一帧 10MB,often 不是 STL 慢,是 API 在 lvalue 路径隐式拿了 ownership。值类别是 move 语义的前置课——搞清表达式类别,才看得懂每一行传的是视图还是所有权。review 时先问 call site:这个表达式有名字吗?能取地址吗?是临时吗?值类别判断错了,后面 std::move 写得再勤也是 copy——因为 move 只能作用于 xvalue/prvalue 路径,const lvalue 绑到 T&& 仍走 copy ctor。

2. 三类值类别(实用版)

lvalue:有名字、可取地址,如变量 cloud。prvalue:纯右值临时或返回 by value 的未命名结果,如 makeCloud()。xvalue:将亡值,std::move(c) 或返回 T&&。C++17 prvalue materialization 少踩坑,最常见仍是把 lvalue 绑到需要 rvalue 的 sink,或 std::move(const Cloud&) 其实仍是 copy——const 剥掉后仍是 lvalue 表达式。auto x = cloud 拷贝;auto&& 绑定左值不自动 move,别指望 forward 在未模板代码里神奇生效。记口诀:有名字多为 lvalue;临时多为 prvalue;move _cast 或 cast 到右值引用产生 xvalue——call site 三问能挡掉八成误 copy。

3. 重载:sink、view、by-value

sink(Cloud&) 修改已有;sink(Cloud&&) 消费临时;read(const Cloud&) 只读。sink(Cloud) by-value 在形参处集中 move/copy 决策——有时比三组 overload 简单。只读路径 const Cloud& 或 span<const Point>,别又存成员。call site sink(cloud) 走 lvalue 可能 copy 进 by-value 形参;sink(makeCloud()) 走 prvalue 可 elide;sink(std::move(cloud)) 走 xvalue 进 Cloud&& 或 by-value move。overload 集不是越多越好——by-value 单参往往覆盖 rvalue 与 lvalue 两条路径,维护成本低于三组重载,前提是 move ctor 便宜且 noexcept。

cpp
void consume(Cloud&& c) { queue_.push(std::move(c)); }
void consume(Cloud c) { queue_.push(std::move(c)); }  // 形参处统一决策
void inspect(const Cloud& c) { /* 不存 c 到成员 */ }

4. std::move 与 move ctor

std::move 是无条件 cast 到 rvalue;无 move ctor 仍 copy。自定义 Buffer 删 copy、留 move,逼 call site 正确。vector::push_back 在 move ctor 非 noexcept 时可能选 copy——大对象 pipeline 里 move ctor 标 noexcept 是性能契约。std::move_if_noexcept 在强异常保证场景才 relevant,机器人 hot path 通常直接要求 noexcept move。move 后读 moved-from 对象:合法但未指定状态,通常只保证析构安全——别对 moved-from vector 做 size() 假设仍有效。

5. 转发引用

template void emplace(T&& v) 里 T&& 是 forwarding reference——对 lvalue 用 std::move(x) 误伤,必须 std::forward<T>(x)。sensor adapter 模板漏 forward 一处就多一次大 copy。range-for 改点用 for (auto& p : cloud),只读用 const auto&。decltype(auto) 返回值别 return {expr} 变 initializer_list 坑。万能引用只出现在 template T&& 且 T 被推导时;void f(Cloud&&) 不是 forwarding reference,是右值引用。

6. 返回值、NRVO 与 ROS callback

return std::move(local) 阻止 NRVO——写 return local。Cloud& get() 指局部则 dangling。ROS callback 按值接 LaserScan msg 每帧 copy 是典型坑——能 loaned/zero-copy 改接口;不能则 move 进 queue。const Cloud& cached() 要保证 backing 寿命覆盖 caller——static local 或 member,别返回临时。prvalue 返回值优化(RVO/NRVO)与 move 语义叠加:多数情况下 return local_name 不必 std::move。C++17 起 prvalue 在某些场景直接 materialize 到目标,少一次 move——但别因此忽略 call site 的 lvalue 误 copy,RVO 救不了「成员按值存 const& 参数」这类 API 设计错误。

7. 失败症状与案例

filter 存 const& 到成员,每帧 copy 百万点——改成 by-value 或 Cloud&& 后同 bag CPU 降一截。move 后仍 read 原对象:UB,ASan 可能抓 moved-from。模板 adapter 漏 forward:profiler 里 copy ctor 在 unexpected 栈帧。验收:-Rpass=move 或断点 copy ctor(若未 delete)。std::move(const Cloud&) 仍 copy 是新人最高频误区之一——类型系统不会因为你写了 move 就转移 const lvalue。另一个高频坑:range-for for (auto p : cloud) 拷贝每个点,改 const auto& 只读或 auto&& 配合算法需求——值类别在循环变量上同样生效。

8. 验收清单

hot path 无意外 copy ctor。move 后不用 moved-from 对象读数据。review:存成员处是值还是引用;callback 参数能否改为 move 或 loaned。新人 onboarding 先画 call site 值类别再写签名——比先写 const& 事后补课省一周。GCC/Clang -Rpass=move 可列出未 elide 的 copy,与断点 copy ctor 互补。pipeline code review 清单可加一行:「本 PR 新增接口是否存 const& 到成员?」——若是,必须说明 backing 寿命或改为 by-value/move,否则默认 reject。

9. 与 pipeline、录包对齐

录包回放若 call site 与线上值类别不一致(例如回放多一次 lvalue 绑定),benchmark 不可比。值类别不是标准背诵——是 pipeline 每一行表达式「能不能 move」的判断依据,和 ownership 箭头一一对应。统一 live 与 replay 的接口形态(by-value / loaned / const view)后再比 CPU,否则优化结论不可信。onboarding 练习:给定十行 pipeline 代码,标出每处表达式的值类别与最终 copy/move 结果——练熟后再写接口,比先写 const& 再补课少踩一周坑。值类别 + ownership 是感知栈 C++ 接口设计的两门必修课,缺任一都会在 profiler 里看到 memcpy。

← 全部文章

johan's blog