那会儿我写的还是个小服务。
代码不多。
但锅一点也不少。
你改个状态。
还得顺手把旧值掏出来。
你以为三行就完事。
它偏偏喜欢在周五晚上给你加戏。
当年没有它:三行代码的暗规则
那时候大家都这么写。 看起来很朴素。 也确实能跑。
int old = x;
x = 42;
use(old);
第一行是“备份”。 第二行是“更新”。 第三行用旧值收尾。
顺序不能乱。 乱了就不是同一件事了。
坑从哪来
麻烦往往不是这三行。 麻烦在你开始“顺手优化”。
变量名变长。 右边从常量变成函数调用。 你就会忍不住把它塞进一个表达式里。
比如这样。
use(x = 42);
这行能编译。 但它已经把“旧值”弄丢了。
你再试着补一个临时变量。
int old = x;
use(x = 42);
也能编译。
但你现在得靠脑子记住两件事:
old 还是不是你要的旧值。
x 到底是什么时候被改掉的。
忙起来的时候。 人真的会记错。
顺手把一个 C 里的细节说清
你可能学过一句话。 “赋值也是表达式。”
在 C++ 里也是。
a = b 这玩意不光会改 a。
它自己还有个“结果”。
这个结果,你可以先粗暴理解成:
“改完之后的 a”。
所以你写 f(a = b) 的时候。
f 拿到的不是旧的 a。
而是新的。
旧的那个值。 一旦被覆盖。 就再也回不来了。
一个小项目线上啪一下
我以前写过一个很小的连接管理。
就一个指针 conn,指向“当前连接”。
断了就重连,旧的要关掉。
最初写得很老实。
void* conn = acquire();
void reconnect() {
void* old = conn;
conn = acquire();
release(old);
}
这段代码笨。 但它把顺序写死了。
后来我想省一行。 就写成了这样。
void reconnect() {
release(conn = acquire());
}
我当时脑子里的意思是: 先换掉。 再把旧的关掉。
但 C/C++ 里,赋值表达式的结果是“新值”。 所以这行代码实际做的是: 关掉新连接。 旧连接留在那儿泄漏。
你把它拆开看。 就更直观。
void reconnect() {
conn = acquire();
release(conn);
}
你关掉的就是刚拿到的那个。 而不是“旧的”。
压测一来。 fd 先爆。 日志里全是超时。
这里的 fd。 你可以把它理解成系统发给你的“连接编号”。 编号用完了。 就啥都连不上了。
你盯着这行代码看十分钟。 才想起自己当年学 C 的那句老话: “表达式是有返回值的。”
C++14 给的答案:std::exchange
后来标准库补了一个小工具。
叫 std::exchange。
你可以把它当成一个“标准库里反复出现的套路”。
尤其是 C++11 之后。 大家开始大量写 move 构造、move 赋值。 到处都是“拿走旧值,然后把对方置空”。
这活儿每个人都手写一遍。 就总会有人写错顺序。
顺便一提。
Boost 也给了一个同名的 boost::exchange。
它就是 std::exchange 的等价实现。
老项目没升到 C++14,也能先用这套写法顶着。
换个语言你也会看到同一类函数。
Rust 有个 mem::replace。
做的也是“塞进去新值,拿出来旧值”。
只不过名字比 C++ 诚实一点。
你看多了会发现。 这不是 C++ 的怪癖。 这是“状态迁移”这件事,本来就需要的一个动作。
它做的事很死板。 先把旧值拿出来。 再把对象改成新值。 最后把旧值还给你。
#include <utility>
void reconnect() {
release(std::exchange(conn, acquire()));
}
这一行读起来就很像人话。
“把 conn 换成新的,把旧的交给 release。”
你不用再跟赋值表达式较劲。 也不用靠注释提醒自己“别写错顺序”。
它到底是怎么做到的
很多新手第一次看到 exchange。
会以为它有什么“魔法”。
其实一点都不玄学。 你用最土的 C 写出来,就是这样。
int exchange_int(int* x, int v) {
int old = *x;
*x = v;
return old;
}
核心就一句话。 先保存。 再覆盖。 最后返回保存的那个。
上面我故意用指针。 因为你学过 C,一眼就懂。
标准库的签名是 T& obj。
这个 T& 叫“引用”。
你可以先把它当成。
“不用写 * 的指针参数”。
它不负责管空不空。
也不负责帮你 free()。
它只是让你写起来更像在改变量本身。
标准库版本多了点模板细节。
会用到 std::move 和 std::forward。
你先记住它们的大白话意思。
std::move(x)。
不是“搬走 x”。
它更像一个强烈的暗示:
“x 现在可以被当成临时对象用。”
有了这个暗示。 如果类型支持 move 构造。 编译器就会优先走“便宜的搬家”。 而不是“昂贵的拷贝”。
std::forward(u) 更像。
“如果你给我的本来就是临时对象,那我就继续当临时对象转交。”
“如果你给我的不是,那我也不瞎改。”
你现在不用把这些背成定义。
只要知道:
exchange 想尽量少拷贝。
想把开销压到最低。
多来几个短例子
先来一个最干净的。 改状态。 顺手看看之前是什么。
enum State { kIdle, kBusy };
State s = kIdle;
State old = std::exchange(s, kBusy);
s 变成 kBusy。
old 里是 kIdle。
再来一个更像线上代码。 只想让“只执行一次”的逻辑变得不容易写错。
if (std::exchange(inited, true) == false) {
init();
}
第一次进来 inited 是 false。
交换后变 true。
所以你知道这次该初始化。
还有一种很常见。 你要把指针置空。 但旧指针得有人接住。
void* p = acquire();
release(std::exchange(p, nullptr));
这行把“置空”和“释放旧的”绑在一起。 你很难在重构时把顺序弄反。
再来一个更 C++ 一点的。
用 std::unique_ptr 管资源。
你要是自己敲一遍。
记得 #include <memory>。
std::unique_ptr<Foo> p = make_foo();
auto old = std::exchange(p, make_foo());
p 现在指向新的。
旧的那份被放进 old。
等 old 离开作用域,它会自动释放。
还有一种也很常见。 你想把一个容器里的东西“一把搬走”。
同理。
这里要 #include <vector>。
std::vector<int> buf = {1, 2, 3};
auto old = std::exchange(buf, {});
buf 变成空。
旧内容在 old 里。
你可以慢慢处理。
它跟 std::swap 有什么不同
std::swap(a, b) 是两边互换。
你得准备一个 b。
std::exchange(a, new_value) 是单边替换。
你把 a 换成你想要的新值。
同时把旧的 a 交出来。
所以它特别像。 “我把状态推进到下一步。” “但我还想知道刚才是哪一步。”
你会在 move 里经常见到它
你学 move(移动语义)的时候,总会遇到一种写法。 move 你可以先粗暴理解成: 把资源从一个对象“搬家”到另一个对象。
说到搬家。 你大概已经猜到了: 搬走之后。 老房子得收拾干净。
这里还有个新手常见误会。
std::move 不是“执行搬家”。
它更像一句话:
“我同意你把它当成临时对象用。”
真正的“搬家动作”。
发生在 move 构造、move 赋值里面。
std::move 只是把开关打开。
顺手补一个背景:RAII 到底是啥
你刚学 C 的时候。
资源基本靠你手写 free()。
写错就泄漏。
C++ 更偏爱另一套习惯。 对象活着就代表资源也活着。 对象死了,就自动清理资源。
这套习惯有个名字。 叫 RAII。 你现在不用背全称。 记住“自动收尾”就行。
ptr = std::exchange(other.ptr, nullptr);
这句的意思其实很朴素。
把 other.ptr 的旧值拿过来。
顺手把 other.ptr 置空。
为什么要置空。 因为被 move 过的对象,最后还会析构。
对象离开作用域。
编译器会自动调用一次析构函数。
你可以把它当成 C 里你手写的 cleanup()。
只不过这里是“语言帮你调用”。
你得保证它“安全析构”,别重复释放同一份资源。
一个很常见的误用:覆盖前没清理
新手写 move 赋值时。 很爱这么写。
ptr = std::exchange(other.ptr, nullptr);
这行“拿走对方的”。 没问题。
问题在于。
你原来这个 ptr 里可能已经有资源了。
你直接覆盖。
旧资源就丢了。
更稳的写法是。 覆盖之前先把旧的处理掉。
void* new_ptr = std::exchange(other.ptr, nullptr);
void* old_ptr = std::exchange(ptr, new_ptr);
release(old_ptr);
这三行很土。 但它在表达同一件事: 先把我这边旧的释放掉。 再接上对方的。 并把对方置空。
你要是偏爱“一行写完”。 也可以这么写。
release(std::exchange(ptr, std::exchange(other.ptr, nullptr)));
如果你觉得这行太绕。 那就老老实实拆成三行。 拆开并不丢人。 丢人的是泄漏。
一句话的结论
std::exchange 就是把那三行“顺序敏感的仪式”,变成一个不太可能写歪的表达式。
最后留个亮点
我后来把它当成一种小签名。
你看到 exchange。
就知道这里不是“随手赋值”。
这里在做状态迁移。 旧值一定有去处。
这比任何 “// 注意顺序” 都靠谱。