每周技巧 #165:带初始化器的 `if` 和 `switch` 语句
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #165: if and switch statements with initializers。
原文最初作为 TotW #165 发布于 2019 年 8 月 17 日。
作者:Thomas Köppe
更新于 2020 年 1 月 17 日。
快捷链接:abseil.io/tips/165
除非你使用条件控制流,否则现在可以停止阅读了。
新语法
C++17 允许 if 和 switch 语句包含初始化器:
|
|
这个语法让你可以尽可能收紧变量作用域:
|
|
初始化器的语义和 for 语句中完全相同;细节见下文。
何时有用
管理复杂性最重要的方法之一,是把复杂系统拆成互不交互、可独立理解并整体忽略的局部部分。在 C++ 中,变量的存在会增加复杂性,而作用域允许我们限制这种复杂性的范围:变量在作用域内出现得越少,读者需要记住该变量存在的次数就越少。
因此,当要求读者投入注意力时,把变量作用域限制在它们实际需要的地方很有价值。新语法为此提供了一个新工具。把这种新语法和 C++17 之前会写的替代代码对比一下:要么我们保持作用域紧凑,但需要写额外花括号:
|
|
要么像更典型的解决方案一样,我们不保持作用域紧凑,只是让变量“泄漏”出去:
|
|
相比之下,新风格是自包含的:不可能在移动 if 语句时不同时移动变量及其作用域。当代码被移动或复制粘贴时,变量的局部含义保持不变。使用以前的风格时,代码移动可能意外改变变量作用域(如果外层花括号没有被复制)、变量含义(如果变量本身没有被复制,且作用域中已有同名变量),或引入名称冲突。
复杂性方面的考虑带来一个常见格言:变量名长度应匹配变量作用域大小;也就是说,作用域越长的变量应该有越长的名字(因为读者走远后仍需要理解它)。反过来,较小作用域允许较短名字。当变量名像上面那样泄漏时,会出现一些令人遗憾的模式:需要多个变量 it1、it2……来避免冲突;变量被重新赋值(auto it = m1.find(/* ... */); it = m2.find(/* ... */));或者变量名变得侵入性地长(auto database_index_iter = m.find(/* ... */))。
细节、作用域、声明区域
if 和 switch 语句中新出现的可选初始化器,工作方式和 for 语句中的初始化器完全相同。(后者本质上是带初始化器的 while 语句。)也就是说,带初始化器语法大体只是下面重写的语法糖:
| Sugared form | Rewritten as |
|---|---|
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 分支中也在作用域内。
不过有一个区别:在语法糖形式中,初始化器和条件、主体(包括 if 和 else 分支)位于同一作用域,而不是位于单独、更大的作用域。这意味着变量名在所有这些部分中必须唯一,不过它们可以遮蔽更早的声明。下面示例展示了各种不允许的重声明和允许的遮蔽声明:
|
|
与结构化绑定交互
C++17 还引入了结构化绑定,这是一种给“可拆解”值(例如 tuple、数组或简单 struct)的元素赋名的机制:auto [iter, ins] = m.insert(/* ... */);
这个特性可以很好地配合 if 语句中的新初始化器:
|
|
另一个例子来自 C++17 的新节点句柄,它允许在 map 或 set 之间真正移动元素而不复制。这个特性定义了一个插入返回类型,该类型可拆解,并且由插入节点句柄产生:
|
|
结论
当你需要一个只在 if 或 switch 语句内部使用、外部不需要的新变量时,请使用新的 if (init; cond) 和 switch (init; cond) 语法。这会简化周围代码。此外,由于变量作用域现在很小,它的名字也可以更短。