Chapter2 带初始化的ifswitch语句

ifswitch语句现在允许在条件表达式里添加一条初始化语句。

例如,你可以写出如下代码:

  1. if (status s = check(); s != status::success) {
  2. return s;
  3. }

其中的初始化语句

  1. status s = check();

初始化了ss将在整个if语句中有效(包括else分支里)。

2.1 带初始化的if语句

if语句的条件表达式里定义的变量将在整个if语句中有效 (包括 then 部分和 else 部分)。例如:

  1. if (std::ofstream strm = getLogStrm(); coll.empty()) {
  2. strm << "<no data>\n";
  3. }
  4. else {
  5. for (const auto& elem : coll) {
  6. strm << elem << '\n';
  7. }
  8. }
  9. // strm不再有效

在整个if语句结束时strm的析构函数会被调用。

另一个例子是关于锁的使用,假设我们要在并发的环境中执行一些依赖某个条件的任务:

  1. if (std::lock_guard<std::mutex> lg{collMutex}; !coll.empty()) {
  2. std::cout << coll.front() << '\n';
  3. }

这个例子中,如果使用类模板参数推导,可以改写成如下代码:

  1. if (std::lock_guard lg{collMutex}; !coll.empty()) {
  2. std::cout << coll.front() << '\n';
  3. }

上面的代码等价于:

  1. {
  2. std::lock_guard<std::mutex> lg{collMutex};
  3. if (!coll.empty()) {
  4. std::cout << coll.front() << '\n';
  5. }
  6. }

细微的区别在于前者中lgif语句的作用域之内定义, 和条件语句在相同的作用域。

注意这个特性的效果和传统for循环里的初始化语句完全相同。 上面的例子中为了让lock_guard生效,必须在初始化语句里明确声明一个变量名, 否则它就是一个临时变量,会在创建之后就立即销毁。因此,初始化一个没有变量名的临时 lock_guard是一个逻辑错误,因为当执行到条件语句时锁就已经被释放了:

  1. if (std::lock_guard<std::mutex>{collMutex}; // 运行时ERROR
  2. !coll.empty()) { // 锁已经被释放了
  3. std::cout << coll.front() << '\n'; // 锁已经被释放了
  4. }

原则上讲,使用简单的_作为变量名就已经足够了:

  1. if (std::lock_guard<std::mutex> _{collMutex}; // OK,但是...
  2. !coll.empty()) {
  3. std::cout << coll.front() << '\n';
  4. }

你也可以同时声明多个变量,并且可以在声明时初始化:

  1. if (auto x = qqq1(), y = qqq2(); x != y) {
  2. std::cout << "return values " << x << " and " << y << "differ\n";
  3. }

或者:

  1. if (auto x{qqq1()}, y{qqq2()}; x != y) {
  2. std::cout << "return values " << x << " and " << y << "differ\n";
  3. }

另一个例子是向map或者unordered map插入元素。 你可以像下面这样检查是否成功:

  1. std::map<std::string, int> coll;
  2. ...
  3. if (auto [pos, ok] = coll.insert({"new", 42}); !ok) {
  4. // 如果插入失败,用pos处理错误
  5. const auto& [key, val] = *pos;
  6. std::cout << "already there: " << key << '\n';
  7. }

这里,我们用了结构化绑定给返回值的成员和pos指向的值的成员声明了新的名称, 而不是直接使用firstsecond成员。在C++17之前,相应的处理代码必须像下面这样写:

  1. auto ret = coll.insert({"new", 42});
  2. if (!ret.second) {
  3. // 如果插入失败,用ret.first处理错误
  4. const auto& elem = *(ret.first);
  5. std::cout << "already there: " << elem.first << '\n';
  6. }

注意这个拓展也适用于编译期if语句特性。

2.2 带初始化的switch语句

通过使用带初始化的switch语句,我们可以在对条件表达式求值之前初始化一个对象/实体。

例如,我们可以先声明一个文件系统路径,然后再根据它的类别进行处理:

  1. namespace fs = std::filesystem;
  2. ...
  3. switch (fs::path p{name}; status(p).type()) {
  4. case fs::file_type::not_found:
  5. std::cout << p << " not found\n";
  6. break;
  7. case fs::file_type::directory:
  8. std::cout << p << ":\n";
  9. for (const auto& e : std::filesystem::directory_iterator{p}) {
  10. std::cout << "- " << e.path() << '\n';
  11. }
  12. break;
  13. default:
  14. std::cout << p << " exists\n";
  15. break;
  16. }

这里,初始化的路径p可以在整个switch语句中使用。