C++20(C++编程语言标准2020版)将是C++语言一次非常重大的更新,将为这门语言引入大量新特性。主要介绍了C++20的Big Four(四大新特性:概念、范围、协程和模块)以及核心语言(包括一些新的运算符和指示符)。
C++20 的编译器支持
查看编译器支持情况:https://en.cppreference.com/w/cpp/compiler_support
简单来说,全新的 GCC、Clang 和 EDG 编译器能提供对核心语言的最佳支持。此外,MSVC 和 Apple Clang 编译器也支持许多 C++20 特性。如下图:
- 新增关键字(keywords)
- 新增标识符(Identifies)
- 模块(Modules)
- Ranges
- 协程(Coroutines)
- Concepts
- Lambda 表达式的更新
- 常量表达式(constexpr) 的更新
- 原子(Atomic)智能指针
- 自动合流(Joining), 可中断(Cancellable) 的线程
- C++20同步(Synchronization)库std::atomic_ref
- 其他更新
- 指定初始化(Designated Initializers)
- 航天飞机操作符 <=>
- 范围 for 循环语句支持初始化
- 非类型模板形参支持字符串
- [[likely]], [[unlikely]]
- 日历(Calendar)和时区(Timezone)功能
- std::span
- 特性测试宏
- consteval 函数
- constinit
- 用 using 引用 enum 类型
- 格式化库(std::format)
- 增加数学常量
- std::source_location
- [[nodiscard(reason)]]
- 位运算
- 一些小更新
新增关键字(keywords)
- concept
- requires
- constinit
- consteval
- co_await
- co_return
- co_yield
- char8_t
新增标识符(Identifies)
- import
- module
模块(Modules)
一直以来 C++ 一直通过引用头文件方式使用库,而其他90年代以后的语言比如 Java、C#、Go 等语言都是通过import
包的方式来使用库。现在C++决定改变这种情况了,在C++20中将引入Modules
,它和Java、Go等语言的包的概念是类似的,直接通过import
包来使用库,再也看不到头文件了。
为什么C++20不再希望使用#include
方式了?因为使用头文件方式存在不少问题,比如有 include 很多模版的头文件将大大增加编译时间,代码生成物也会变大。而且引用头文件方式不利于做一些 C++ 库和组件的管理工具,尤其是对于一些云环境和分布式环境下不方便管理,C++一直缺一个包管理工具,这也是C++被吐槽得很多的地方,现在C++20 Modules将改变这一切。
每个Module由Module接口和实现组成,接口只有一份,实现可以有多份。
Modules 接口和实现的语法:
1 | export module module_name; |
使用:
1 | import module_name; |
Modules 允许你导出类,函数,变量,常量和模版等等。接下来看一个使用 Modules 的例子:
1 | // main.cpp |
创建和使用自定义的module:
1 | // 创建模块 |
Ranges
- Range代表一串元素, 或者一串元素中的一段;
- 类似begin/end对;
相比STL,Ranges是更高一层的抽象,Ranges对STL做了改进,它是STL的下一代。为什么说Ranges是STL的未来?虽然 STL在C++中提供的容器和算法备受推崇和广泛被使用,但STL一直存在两个问题:
- STL 强制你必须传一个 begin 和 end 迭代器用来遍历一个容器;
- STL 算法不方便组合在一起。
STL 必须传迭代器,这个迭代器仅仅是辅助你完成遍历序列的技术细节,和我们的函数功能无关,大部分时候我们需要的是一个 range,代表的是一个比迭代器更高层的抽象。那么 Ranges 到底是什么呢?Ranges 是一个引用元素序列的对象,在概念上类似于一对迭代器。这意味着所有的 STL 容器都是 Ranges。在 Ranges 里我们不再传迭代器了,而是传 range。比如下面的代码:
1 | // STL写法 |
相关功能
视图(View): 延迟计算, 不持有, 不改写;
1
2
3
4
5
6
7std::vector<int> data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = data |
ranges::views::remove_if([](int i) { return i % 2 == 1;}) |
ranges::views::transform([](int i) { return std::to_string(i);});
// result = {"2", "4", "6", "8", "10" };
// 注意 以上操作被延迟, 当你遍历result的时候才触发Actions: 即时处理(eagerly evaluated), 改写;
1
2
3
4
5std::vector<int> data{4, 3, 4, 1, 8, 0, 8};
std::vector<int> result = data | ranges::actions::sort | ranges::actions::unique;
// 排序然后去重
// 操作会原地对data进行更改, 然后返回Algorithms: 所有接受begin/end对的算法都可用;
views和actions使用管道符|串联。
过滤和变换
1 | int total = accumulate( |
协程(Coroutines)
很多语言提供了 Coroutine 机制,因为 Coroutine 可以大大简化异步网络程序的编写,现在 C++20 中也要加入协程了,但是不幸的是c++20的协程标准只包含编译器需要实现的底层功能,并没有包含简单方便地使用协程的高级库,相关的类和函数进入std标准库估计要等到c++23。所以,在c++20中,如果要使用协程,要么等别人封装好了给你用,要么就要自己学着用底层的功能自己封装。
如果不用协程,写一个异步的网络程序是不那么容易的,以 boost.asio 的异步网络编程为例,我们需要注意的地方很多,比如异步事件完成的回调函数中需要保证调用对象仍然存在,如何构建异步回调链条等等,代码比较复杂,而且出了问题也不容易调试。而协程给我们提供了对异步编程优雅而高效的抽象,让异步编程变得简单!
C++ Courotines 中增加了三个新的关键字:co_await,co_yield 和 co_return,如果一个函数体中有这三个关键字之一就变成 Coroutine 了。co_await 用来挂起和恢复一个协程,co_return 用来返回协程的结果,co_yield 返回一个值并且挂起协程。协程说到底就是一个函数,使用for co_await (for-range-declaration: expression) statement
循环体方式。简化如下问题的实现:
- generator;
- 异步I/O;
- 延迟计算;
- 事件驱动的程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15experimental::generator<int> GetSequenceGenerator(
int startValue, size_t numberOfValues) {
for (int i = 0 startValue; i < startValue + numberOfValues; ++i){
time_t t = system_clock::to_time_t(system_clock::now());
cout << std::ctime(&t);
co_yield i;
}
}
int main() {
auto gen = GetSequenceGenerator(10, 5);
for (const auto& value : gen) {
cout << value << "(Press enter for next value)" << endl;
cin.ignore();
}
}
Concepts
- 对模板类和函数的模板形参的约束;
- 编译期断言;
- 可声明多个。
1
template<typename T> concept Incrementable = requires(T x) {x++; ++x;};
常量表达式(constexpr) 的更新
- constexpr 虚函数
- constexpr 的虚函数可以重写非 constexpr 的虚函数
- 非 constexpr 虚函数可以重写 constexpr 的虚函数
- constexpr 函数可以:
- 使用 dynamic_cast() 和 typeid
- 动态内存分配
- 更改union成员的值
- 包含 try/catch, 但是不允许throw语句, 在触发常量求值的时候 try/catch 不发生作用,需要开启 constexpr std::vector
constexpr string & vector: std::string和std::vector类型现在可以作为constexpr。
C++17 Library Features
std::variant
The class template std::variant represents a type-safe union. An instance of std::variant at any given time holds a value of one of its alternative types (it’s also possible for it to be valueless).
1 | std::variant<int, double> v{ 12 }; |
std::optional
The class template std::optional manages an optional contained value, i.e. a value that may or may not be present. A common use case for optional is the return value of a function that may fail.
1 | std::optional<std::string> create(bool b) { |
std::any
A type-safe container for single values of any type.
1 | std::any x {5}; |
std::string_view
A non-owning reference to a string. Useful for providing an abstraction on top of strings (e.g. for parsing).
1 | // Regular strings. |
1 | std::string str {" trim me"}; |
Splicing for maps and sets
Moving nodes and merging containers without the overhead of expensive copies, moves, or heap allocations/deallocations.
Moving elements from one map to another:
1 | std::map<int, string> src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}}; |
Inserting an entire set:
1 | std::set<int> src {1, 3, 5}; |
Inserting elements which outlive the container:
1 | auto elementFactory() { |
Changing the key of a map element:
1 | std::map<int, string> m {{1, "one"}, {2, "two"}, {3, "three"}}; |
Parallel algorithms
Many of the STL algorithms, such as the copy, find and sort methods, started to support the parallel execution policies: seq, par and par_unseq which translate to “sequentially”, “parallel” and “parallel unsequenced”.
1 | std::vector<int> longVector; |