每周技巧 #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 允许 ifswitch 语句包含初始化器:

1
2
if (init; cond) { /* ... */ }
switch (init; cond) { /* ... */ }

这个语法让你可以尽可能收紧变量作用域:

1
2
3
4
5
if (auto it = m.find("key"); it != m.end()) {
  return it->second;
} else {
  return absl::NotFoundError("Entry not found");
}

初始化器的语义和 for 语句中完全相同;细节见下文。

何时有用

管理复杂性最重要的方法之一,是把复杂系统拆成互不交互、可独立理解并整体忽略的局部部分。在 C++ 中,变量的存在会增加复杂性,而作用域允许我们限制这种复杂性的范围:变量在作用域内出现得越少,读者需要记住该变量存在的次数就越少。

因此,当要求读者投入注意力时,把变量作用域限制在它们实际需要的地方很有价值。新语法为此提供了一个新工具。把这种新语法和 C++17 之前会写的替代代码对比一下:要么我们保持作用域紧凑,但需要写额外花括号:

1
2
3
4
5
6
7
8
{
  auto it = m.find("key");
  if (it != m.end()) {
    return it->second;
  } else {
    return absl::NotFoundError("Entry not found");
  }
}

要么像更典型的解决方案一样,我们保持作用域紧凑,只是让变量“泄漏”出去:

1
2
3
4
5
6
auto it = m.find("key");
if (it != m.end()) {
  return it->second;
} else {
  return absl::NotFoundError("Entry not found");
}

相比之下,新风格是自包含的:不可能在移动 if 语句时不同时移动变量及其作用域。当代码被移动或复制粘贴时,变量的局部含义保持不变。使用以前的风格时,代码移动可能意外改变变量作用域(如果外层花括号没有被复制)、变量含义(如果变量本身没有被复制,且作用域中已有同名变量),或引入名称冲突。

复杂性方面的考虑带来一个常见格言:变量名长度应匹配变量作用域大小;也就是说,作用域越长的变量应该有越长的名字(因为读者走远后仍需要理解它)。反过来,较小作用域允许较短名字。当变量名像上面那样泄漏时,会出现一些令人遗憾的模式:需要多个变量 it1it2……来避免冲突;变量被重新赋值(auto it = m1.find(/* ... */); it = m2.find(/* ... */));或者变量名变得侵入性地长(auto database_index_iter = m.find(/* ... */))。

细节、作用域、声明区域

ifswitch 语句中新出现的可选初始化器,工作方式和 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 分支中也在作用域内。

不过有一个区别:在语法糖形式中,初始化器和条件、主体(包括 ifelse 分支)位于同一作用域,而不是位于单独、更大的作用域。这意味着变量名在所有这些部分中必须唯一,不过它们可以遮蔽更早的声明。下面示例展示了各种不允许的重声明和允许的遮蔽声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int w;

if (int x, y, z; int y = g()) {   // error: y redeclared, first declared in initializer
  int x;                          // error: x redeclared, first declared in initializer
  int w;                          // OK, shadows outer variable
  {
    int x, y;                     // OK, shadowing in nested scope is allowed
  }
} else {
  int z;                          // error: z redeclared, first declared in initializer
}

if (int w; int q = g()) {         // declaration of "w" OK, shadows outer variable
  int q;                          // error: q redeclared, first declared in condition
  int w;                          // error: w redeclared, first declared in initializer
}

与结构化绑定交互

C++17 还引入了结构化绑定,这是一种给“可拆解”值(例如 tuple、数组或简单 struct)的元素赋名的机制:auto [iter, ins] = m.insert(/* ... */);

这个特性可以很好地配合 if 语句中的新初始化器:

1
2
3
4
5
if (auto [iter, ins] = m.try_emplace(key, data); ins) {
  use(iter->second);
} else {
  LOG(ERROR) << "Key '" << key << "' already exists.";
}

另一个例子来自 C++17 的新节点句柄,它允许在 map 或 set 之间真正移动元素而不复制。这个特性定义了一个插入返回类型,该类型可拆解,并且由插入节点句柄产生:

1
2
3
4
5
6
7
if (auto [iter, ins, node] = m2.insert(m1.extract(k)); ins) {
  LOG(INFO) << "Element with key '" << k << "' transferred successfully";
} else if (!node) {
  LOG(ERROR) << "Key '" << k << "' does not exist in first map.";
} else {
  LOG(ERROR) << "Key '" << k << "' already in m2; m2 unchanged; m1 changed.";
}

结论

当你需要一个只在 ifswitch 语句内部使用、外部不需要的新变量时,请使用新的 if (init; cond)switch (init; cond) 语法。这会简化周围代码。此外,由于变量作用域现在很小,它的名字也可以更短。

每周技巧 #163:传递 `std::optional` 参数

上一节

每周技巧 #166:当拷贝不是拷贝

下一节