第18章 支持初始化语句的if和switch(C++17)

在C++17标准中,if控制结构可以在执行条件语句之前先执行一个初始化语句。语法如下:

if (init; condition) {}

其中init是初始化语句,condition是条件语句,它们之间使用分号分隔。允许初始化语句的if结构让以下代码成为可能:

#include <iostream>
bool foo()
{
  return true;
}
int main()
{
  if (bool b = foo(); b) {
       std::cout << std::boolalpha << "good! foo()=" << b << std::endl;
  }
}

在上面的代码中,bool b = foo()是一个初始化语句,在初始化语句中声明的变量b能够在if的作用域继续使用。事实上,该变量的生命周期会一直伴随整个if结构,包括else ifelse部分。

if初始化语句中声明的变量拥有和整个if结构一样长的声明周期,所以前面的代码可以等价于:

#include <iostream>
bool foo()
{
  return true;
}
int main()
{
  {
       bool b = foo();
       if (b) {
            std::cout << std::boolalpha << "good! foo()=" << b << std::endl;
       }
  }
}

当然,我们还可以在if结构中添加else部分:

  if (bool b = foo(); b) {
       std::cout << std::boolalpha << "good! foo()=" << b << std::endl;
  }
  else {
       std::cout << std::boolalpha << "bad! foo()=" << b << std::endl;
  }

if结构中引入else if后,情况会稍微变得复杂一点,因为在else if条件语句之前也可以使用初始化语句:

#include <iostream>
bool foo()
{
  return false;
}
bool bar()
{
  return true;
}
int main()
{
  if (bool b = foo(); b) {
       std::cout << std::boolalpha << "foo()=" << b << std::endl;
  }
  else if (bool b1 = bar(); b1) {
       std::cout << std::boolalpha 
            << "foo()=" << b
            << ", bar()=" << b1 << std::endl;
  }
}

在上面的代码中,ifelse if都有初始化语句,它们分别初始化变量bb1并且在各自条件成立的作用域内执行了日志输出。值得注意的是,bb1的生命周期并不相同。其中变量b的生命周期会贯穿整个if结构(包括else if),可以看到在else if中也能引用变量b。但是b1则不同,它的生命周期只存在于else if以及后续存在的else ifelse语句,而无法在之前的if中使用,等价于:

{
  bool b = foo();
  if (b) {
       std::cout << std::boolalpha << "foo()=" << b << std::endl;
  }
  else {
       bool b1 = bar();
       if (b1) {
            std::cout << std::boolalpha
                 << "foo()=" << b
                 << ", bar()=" << b1 << std::endl;
       }
  }
}

因为if初始化语句声明的变量会贯穿整个if结构,所以我们可以利用该特性对整个if结构加锁,例如:

#include <mutex>
std::mutex mx;
bool shared_flag = true;
int main()
{
  if (std::lock_guard<std::mutex> lock(mx); shared_flag) { 
       shared_flag = false;
  }
}

继续扩展思路,从本质上来说初始化语句就是在执行条件判断之前先执行了一个语句,并且语句中声明的变量将拥有与if结构相同的生命周期。所以我们在代码中没有必要一定在初始化语句中初始化判断条件的变量,如if(std::lock_guard <std::mutex> lock(mx); shared_flag),初始化语句并没有初始化条件判断的变量shared_flag。类似的例子还有:

#include <cstdio>
#include <string>
int main()
{
  std::string str;
  if (char buf[10]{0}; std::fgets(buf, 10, stdin)) {
       str += buf; 
  }
}

在上面的代码中,if的初始化语句只声明了一个数组buf并将buf作为实参传入std::fgets函数,而真正做条件判断的是std::fgets函数返回值。

if控制结构一样,switch在通过条件判断确定执行的代码分支之前也可以接受一个初始化语句。不同的是,switch结构不存在elseelse if的情况,所以语法更加简单。这里以std::condition_variable为例,其成员函数wait_for需要一个std:: unique_lock<std::mutex>&类型的实参,于是在switch的初始化语句中可以构造一个std::unique_lock<std::mutex>类型的对象,具体代码如下:

#include <condition_variable>
#include <chrono>
using namespace std::chrono_literals;
std::condition_variable cv;
std::mutex cv_m;
int main()
{
  switch (std::unique_lock<std::mutex> lk(cv_m); cv.wait_for(lk, 100ms))
  {
  case std::cv_status::timeout:
       break;
  case std::cv_status::no_timeout:
       break;
  }
}

switch初始化语句声明的变量的生命周期会贯穿整个switch结构,这一点和if也相同,所以变量lk能够引用到任何一个case的分支中。

读者应该已经注意到,所谓带初始化语句的if和switch的新特性只不过是一颗语法糖而已,其带来的功能可以轻易地用等价代码代替,但是C++委员会还是决定将该特性引入C++17标准。其中的一个原因是该特性并非是全新的语法,在for循环中已经存在类似的语法了,而且新增语法也不会增加语法的复杂度,所以无论是学习成本还是使用成本都是很低的。另外,使用该特性的等价代码并非是一种好的解决方案,因为增加大量的大括号和缩进并不利于代码的阅读和维护;而如果不增加大括号和缩进又会导致初始化代码声明的变量入侵ifswitch以外的作用域,如此一来在代码整理和重构的时候可能会出现问题。因此将初始化语句和条件语句写在一行确实有助于代码阅读和整理,与此同时也能减少无谓的大括号和缩进,增加代码的可读性和可维护性。