C++17/20新特性 支持初始化语句的if、switch、范围for

在项目开发中我们会用到STL中各种容器,假设我们有一个 map,我们希望查找一个元素,并在找到时执行某些操作:

1
2
3
4
5
6
7
std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};

auto it = m.find(2);
if (it != m.end())
{
std::cout << "Found: " << it->second << std::endl;
}

上面代码虽然能够完成我们的要求,但有如下问题:

  • 作用域污染:it变量在if语句之后仍能被继续使用,会造成作用域污染。
  • 简洁性与可维护性差:代码不够简洁,并且当it迭代器失效时继续使用会有未知风险。

1. if

C++ 17中,if 可以在执行条件判断之前先执行一个初始化语句。在该语句中可以执行某个表达式语句、初始化变量等。

语法:

1
2
3
4
5
6
if (init-statement; condition) 
...
else if (init-statement; condition)
...
else
...

说明:

  1. init-statement与condition通过 ; 分隔,init-statement可以为空。
  2. 如果在init-statement中初始化变量,该变量生命周期(作用域)从当前init-statement开始,直到整个if结束。
  3. 可以在init-statement中初始化多个相同类型变量,它们通过 , 分隔。

当if支持初始化语句时,我们可以将上面代码改为:

1
2
3
4
5
6
std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};

if (auto it = m.find(2); it != m.end())
{
std::cout << "Found: " << it->second << std::endl;
}

在上文中我们说到,在 init-statement中初始的变量,该变量生命周期从当前开始,直到整个if结束。所以上面代码可以等价于:

1
2
3
4
5
6
7
8
9
std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};

{
auto it = m.find(2);
if (it != m.end())
{
std::cout << "Found: " << it->second << std::endl;
}
}

下面举一个稍微复杂的例子,在map中存放用户名与对应角色,给定一个用户名,根据不同角色输出不同欢迎信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <map>
#include <string>

int main()
{
std::map<std::string, std::string> user_roles = { {"alice", "admin"}, {"bob", "user"}, {"eve", "guest"}};
std::string username = "alice";

if (auto it = user_roles.find(username); it == user_roles.end()) // it生命周期从当前位置开始直到整个if结束
{
std::cout << "User not found.\n";
}
else if (auto role = it->second; "admin" == role) // role生命周期从当前位置开始直到整个if结束
{
std::cout << "Welcome, admin " << username << "!\n";
}
else if ("user" == role)
{
std::cout << "Hello, user " << username << ".\n";
}
else
{
std::cout << "Access limited for guest " << username << ".\n";
}

return 0;
}

上面代码可以等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
auto it = user_roles.find(username);
if (it == user_roles.end())
{
std::cout << "User not found.\n";
}
else
{
auto role = it->second;
if ("admin" == role)
{
std::cout << "Welcome, admin " << username << "!\n";
}
else if ("user" == role)
{
std::cout << "Hello, user " << username << ".\n";
}
else
{
std::cout << "Access limited for guest " << username << ".\n";
}
}
}

2. switch

C++ 17中,switchif一样,可以在执行条件判断之前先执行一个初始化语句。

语法:

1
2
3
4
5
6
7
8
9
10
11
switch (init-statement; condition-expression) 
{
case value1:
...
break;
case value2:
...
break;
default:
...
}

注意:如果在init-statement中初始化变量,该变量生命周期(作用域)会贯穿整个switch结构。

下面这个例子模拟主线程检查一个后台任务是否在规定时间内完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <chrono>

using namespace std::chrono_literals;

int main()
{
std::mutex mtx;
std::condition_variable cv;
bool ready = false;

// 后台线程在 500ms 后设置 ready = true 并通知
std::thread notifier([&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
{
std::lock_guard<std::mutex> lock(mtx);
// 任务处理
ready = true;
}
cv.notify_one();
});


// 检查一个任务100ms内是否完成
switch (std::unique_lock<std::mutex> lock(mtx); cv.wait_for(lock, 100ms))
{
case std::cv_status::timeout:
std::cout << "Timeout: did not get notification in 100ms.\n";
break;

case std::cv_status::no_timeout:
std::cout << "Got notification before timeout.\n";
break;
}

notifier.join();

return 0;
}

3. 范围for

C++ 20中,范围 for 引入了一个新特性:初始化语句。简单来说,就是你可以在范围 for 里像普通 for循环那样先写一个初始化语句,然后再写循环。

语法:

1
2
for (init-statement; item-declaration : range-initializer)
statement

说明:

  1. init-statement与item-declaration通过 ; 分隔,init-statement可以为空。

  2. 如果在init-statement中初始化变量,该变量生命周期(作用域)直到整个范围for结束。

  3. 可以在init-statement中初始化多个相同类型变量,它们通过,分隔。

在开发中我经常有个需求,需要打印容器中每个元素的值以及它的下标。

(1) 使用传统for循环:

1
2
3
4
5
6
std::vector<int> vec{10, 20, 30};

for (int i = 0; i < vec.size(); i++)
{
std::cout << "vec[" << i << "]=" << vec[i] << std::endl;
}

(2) 使用范围for

1
2
3
4
5
6
7
8
std::vector<int> vec{10, 20, 30};

int i = 0; // for循环结束后会存在命名空间污染问题
for (auto &e: vec)
{
std::cout << "vec[" << i << "]=" << vec[i] << std::endl;
i++;
}

(3) 使用迭代器

1
2
3
4
5
6
7
8
std::vector<int> vec{10, 20, 30};

int i = 0; // while循环结束后会存在命名空间污染问题
auto it = vec.begin(); // 命名空间污染与迭代器失效后误使用问题
while (it != vec.end())
{
std::cout << "vec[" << i++ << "]=" << *it++ << std::endl;
}

我们发现不管使用哪种方式代码都不是很优雅,在c++ 20中我们可以这样写,代码是不是非常优雅了!

1
2
3
4
for (int i = 0; auto &e: vec)
{
std::cout << "vec[" << i++ << "]=" << e << std::endl;
}

使用注意

goto跳过局部变量的初始化会编译错误,所以下面代码不正确:

1
2
3
4
5
6
7
goto label;  // 跳过局部变量x初始化,编译错误

if (int x = 1; x == 1)
{
label:
std::cout << "x = " << x << std::endl;
}

总结: 通过上面示例可以看出,在if、switch、范围for中所谓支持初始化语句的新特性其实就是语法糖而已,我们可以轻易地用等价代码代替。C++ 新标准之所以引入它们很重要原因是可以提高代码可读性以及避免作用域污染。