(原文链接:https://abseil.io/tips/165 译者:clangpp@gmail.com)
每周贴士 #165: 带初始化器的if
和switch
语句
- 最初发布于:2019-08-17
- 作者:Thomas Köppe
- 更新于:2020-01-17
- 短链接:abseil.io/tips/165
如果你用不到条件控制流程,那可以到此为止了。
一个新语法
C++17 允许 if
和switch
语句包含初始化器:
if (init; cond) { /* ... */ }
switch (init; cond) { /* ... */ }
这种语法可以让你的变量作用域尽量地小:
if (auto it = m.find("key"); it != m.end()) {
return it->second;
} else {
return absl::NotFoundError("Entry not found");
}
这里初始化器的语义和for
语句里的完全一样;下面会详细说明。
什么时候用
管理复杂度最重要的方式之一,就是把复杂系统拆解成为互不影响的,本地化的组件,每个组件都可以被单独理解,并且作为整体被忽略。在C++中,变量的存在增加了复杂度,而 作用域 允许我们限制由此带来的复杂度:变量所在的作用域越小,读者需要记住的变量就越少。
当需要读者关注的时候,将变量作用域限制在其被使用的地方就变得重要了。这个新语法为此提供了新工具。对比这个新语法和C++17以前的代码:要么我们保持作用域尽量小,为此需要多写一对大括号:
{
auto it = m.find("key");
if (it != m.end()) {
return it->second;
} else {
return absl::NotFoundError("Entry not found");
}
}
要么,正如看起来更典型的方案,我们 并不 保持作用域尽量小,就“泄露”变量:
auto it = m.find("key");
if (it != m.end()) {
return it->second;
} else {
return absl::NotFoundError("Entry not found");
}
这种复杂的考量催生了常见的程序员黑话:变量名长度应该与作用域大小相匹配;也就是说,作用域越大,其中的变量名就该越长(为了让读者走出二里地还能记着它)。反之,作用域越小,其中的变量名就可以越短。当变量名“泄露”的时候(如上所示),我们经常看见坑爹的模式涌现出来,例如:多个变量it1
,it2
,…变得必要以防止命名冲突;变量被重新赋值(auto it = m1.find(/* ... */); it = m2.find(/* ... */)
);或变量有个侵入式的长名字(auto database_index_iter = m.find(/* ... */)
)。
细节,作用域,声明区
这个新的,可选的if
和switch
语句中的初始化器,其工作方式与for
语句里的初始化器完全一致。(后者基本是一个带初始化器的while
语句。)也就是说,这个含有初始化器的语法基本就是如下写法的语法糖:
语法糖格式 | (译者注:自动)改写成为 |
---|---|
if (init; cond) BODY | { init; if (cond) BODY } |
switch (init; cond) BODY | { init; switch (cond) BODY } |
for (init; cond; incr) BODY | { init; while (cond) { BODY; incr; } |
重点提一句,在初始化器中声明的变量名,在if
语句可能存在的else
分支中仍然有效。
当然,还是有一点不同:在语法糖格式里,初始化器跟条件(condition)或主体(body)(既包含if
分支,又包含else
分支)在同一作用域里,而不是在另一个,更大的作用域。这意味着变量名在这一堆组件里必须保持唯一,当然它们可以遮蔽掉更早的声明。下面的例子阐释了各种禁止的重定义和允许的遮蔽声明:
int w;
if (int x, y, z; int y = g()) { // 错误:y重定义了,初始化器中定义过了
int x; // 错误:x重定义了,初始化器中定义过了
int w; // 可以,遮蔽掉外层变量
{
int x, y; // 可以,在嵌套作用域中遮蔽变量是允许的
}
} else {
int z; // 错误:z重定义了,初始化器中定义过了
}
if (int w; int q = g()) { // w的声明可以,遮蔽掉外层变量
int q; // 错误:q重定义了,在条件语句中定义过了
int w; // 错误:w重定义了,在初始化器中定义过了
}
与结构化绑定(structured bindings)的交互
C++17也引入了 结构化绑定,这种机制可以用来为“可解构的”值(例如元组、数组或简单的结构体)其中的元素命名:auto [iter, ins] = m.insert(/* ... */);
这个特性和if
语句里的初始化器相得益彰:
if (auto [iter, ins] = m.try_emplace(key, data); ins) {
use(iter->second);
} else {
std::cerr << "Key '" << key << "' already exists.";
}
另一个例子来自C++17新引入的 节点句柄(node handles),可以用来在map和set之间移动元素(而不是拷贝)。这个特性定义了一个可解构的 插入返回值(insert-return-type),作为插入一个节点句柄的返回值。
if (auto [iter, ins, node] = m2.insert(m1.extract(k)); ins) {
std::cout << "Element with key '" << k << "' transferred successfully";
} else if (!node) {
std::cerr << "Key '" << k << "' does not exist in first map.";
} else {
std::cerr << "Key '" << k << "' already in m2; m2 unchanged; m1 changed.";
}
结论
在以下情况下使用新的if (init; cond)
和switch (init; cond)
语法:当你需要一个新变量,在if
和switch
语句内部用到,且在此之外不被用到的时候。这可以简化周围的代码。另外,变量的作用域小了,变量名也可以起得更短。