我见过最离谱的一次线上事故。
不是内存越界。
不是死锁。
是一个迭代器。
它本来只该活在一个 if 里。
结果它活到了后面。
然后我在 debug 分支里顺手用了它。
线上就啪一下。
这篇文章不背语法。
只把一件事讲透。
怎么把“只应该活一会儿的变量”。
关进 if / switch 的作用域里。
先补一个最容易混的点:名字能被看见的范围
你在 C 里已经见过“块作用域”。
大括号里面声明的东西,出了大括号就看不见。
if (ok) {
int x = 1;
}
use(x);
上面这段,use(x) 编译不过。
因为 x 的名字只在那对大括号里可见。
而我们今天要解决的事故,其实就是反过来。
我以为变量“只活在 if 里”。
但它的名字其实在 if 后面还可见。
旧写法长什么样:先声明,再判断
你想在 map 里找一个 key。
最直觉的写法是两行。
auto it = m.find(key);
if (it != m.end()) {
use(it->second);
}
这段逻辑没问题。
但 it 的名字会一直存在到当前这层 {} 结束。
也就是:它会溜到 if 后面。
最小复现:变量溜出去以后,你很容易“误用旧值”
下面这个例子小到不能再小。
auto it = headers.find("token");
if (it != headers.end()) {
use_token(it->second);
}
if (need_debug) {
log(it->second);
}
当 token 不存在时,it == headers.end()。
你在 debug 分支里一解引用。
就等着出事。
更坑的是:这段代码“看起来像没问题”。
因为你脑子里已经把 it 当成 if 的局部变量了。
旧办法 1:强行加作用域,能用,但太脆
第一种补丁是再套一层 {}。
{
auto it = headers.find("token");
if (it != headers.end()) {
use_token(it->second);
}
}
这样 it 确实出不来了。
问题也很工程:它太像“多余的括号”。
团队里总有人会顺手删掉。
删完还不报错。
旧办法 2:先判断再声明,代价是重复查找
你也可以先判断。
然后在分支里再声明。
if (m.find(key) != m.end()) {
auto it = m.find(key);
use(it->second);
}
这段更稳。
但你查了两次。
如果这段在热路径上,性能会很真实。
而且你把“我需要 iterator”这层意图拆散了。
C++17 的新写法:if (初始化; 条件)
这就是 C++17 的 if 初始化语句。
语法长这样。
if (auto it = m.find(key); it != m.end()) {
use(it->second);
}
分号前面是一条“初始化语句”。
分号后面才是条件。
你可以把它理解成:语言帮你自动加了一层隐藏的大括号。
所以 it 的名字只在这条 if 语句里可见。
包括 else。
if (auto it = headers.find("token"); it != headers.end()) {
use_token(it->second);
} else {
use_anonymous();
}
但出了这条 if。
it 就真的不存在了。
你后面再写 it,直接编译不过。
这就是它最管用的地方。
把“别误用”变成编译期错误。
再拆一个容易卡的点:为什么 if (auto it = m.find(key)) 不行
很多刚从 C 过来的人,会本能写成这样。
if (auto it = m.find(key)) {
use(it->second);
}
这在 C 里像是在判断指针是否为 NULL。
但 iterator 不是指针。
它没有“空”的 bool 语义。
所以 C++ 才把 initializer 设计成“初始化 + 条件”两段式。
你初始化可以很复杂。
条件也必须写得明确。
switch 也一样:switch (初始化; 表达式)
switch 以前也经常让变量外泄。
int code = parse_code(msg);
switch (code) {
case 200:
handle_ok();
break;
case 404:
handle_not_found();
break;
}
code 在 switch 外面活着。
后面就可能被复用。
或者被你误当成“这次解析的结果”。
C++17 允许你把它关进去。
switch (int code = parse_code(msg); code) {
case 200:
handle_ok();
break;
case 404:
handle_not_found();
break;
}
code 只属于这次 switch。
switch 结束,它也结束。
这套语法学谁的:for 早就这么干了(C99 也走过这条路)
其实你早就习惯了“初始化变量只活在一段语句里”。
比如 for。
for (int i = 0; i < n; ++i) {
use(i);
}
循环结束,i 就没了。
没人指望在循环外继续用 i。
if/switch initializer 只是在说:分支语句也应该有这个能力。
横向看,C 这边也是这么演进的。
老 C 要求你把 int i; 放在块开头。
后来 C99 才允许 for (int i = 0; ...)。
动机很一致:减少变量污染。
一个很实用的例子:把锁的寿命也关进 if 里
initializer 不只适合 iterator。
它还特别适合 RAII 对象。
比如锁。
if (std::lock_guard<std::mutex> lk(mu); ready()) {
consume();
}
这段的直觉是:我只想在判断和处理这段期间持锁。
if 结束,锁也立刻释放。
对比一下旧写法。
std::lock_guard<std::mutex> lk(mu);
if (ready()) {
consume();
}
这段也能用。
但 lk 会活到当前作用域结束。
你很容易不小心把锁持有时间拉长。
边界与坑点:initializer 会执行,变量也只活在这条语句里
initializer 的一个代价是:它一定会执行。
所以别在里面塞特别重的初始化。
也别在里面写你不想发生的副作用。
另一个代价更直接。
如果你确实要在 if 之后继续用那个变量。
那你就不该把它关进去。
把“它还活着”变成有意识的选择。
你可以直接照抄的判断规则
你只要问自己一句话。
这个变量离开 if/switch 之后,还应该被用到吗?
如果不应该。
就把它放进 initializer。
让编译器替你看门。
临时变量就该短命。
别让它活到下一段。
以后你看到 if (init; cond)。
就把它当成一句话。
用完。
关门。