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
2
export module module_name;
module module_name;

使用:

1
import module_name;

Modules 允许你导出类,函数,变量,常量和模版等等。接下来看一个使用 Modules 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
// main.cpp
import std.vector;
import std.string;
import std.iostream;
import std.iterator;

using namespace std;
int main() {
std::vector<std::string> v = {"a", "b"};
copy(begin(v), end(v), std::ostream_iterator<std::string>(std::cout, "\n"));
return 0;
}

创建和使用自定义的module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建模块
// demo.cpp
export module demo;
namespace Demo {
auto GetWelcomeHelper() { return "Welcome!"; }
export auto GetWelcome() { return GetWelcomeHelper();}
}

// 引用模块
// main.cpp
import cppcon;
int main(){
std::cout << Demo::GetWelcome();
}

Ranges

  • Range代表一串元素, 或者一串元素中的一段;
  • 类似begin/end对;

相比STL,Ranges是更高一层的抽象,Ranges对STL做了改进,它是STL的下一代。为什么说Ranges是STL的未来?虽然 STL在C++中提供的容器和算法备受推崇和广泛被使用,但STL一直存在两个问题:

  1. STL 强制你必须传一个 begin 和 end 迭代器用来遍历一个容器;
  2. STL 算法不方便组合在一起。

STL 必须传迭代器,这个迭代器仅仅是辅助你完成遍历序列的技术细节,和我们的函数功能无关,大部分时候我们需要的是一个 range,代表的是一个比迭代器更高层的抽象。那么 Ranges 到底是什么呢?Ranges 是一个引用元素序列的对象,在概念上类似于一对迭代器。这意味着所有的 STL 容器都是 Ranges。在 Ranges 里我们不再传迭代器了,而是传 range。比如下面的代码:

1
2
3
4
5
6
// STL写法
std::vector<int> data{11, 22, 33};
std::sort(data.begin(), data.end());

// Ranges写法
std::sort(data);

相关功能

  • 视图(View): 延迟计算, 不持有, 不改写;

    1
    2
    3
    4
    5
    6
    7
    std::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
    5
    std::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
2
3
4
5
6
7
8
9
10
int total = accumulate(
ranges::view::ints(1) |
ranges::view::transform([](int i) {return i * i;}) |
ranges::view::take(10),
0);

// view::ints(1)产生一个无限的整型数列
// 计算平方
// 取前10个元素, 然后累加(accumulate)
// 所有的计算延迟到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
    15
    experimental::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
2
3
4
5
6
std::variant<int, double> v{ 12 };
std::get<int>(v); // == 12
std::get<0>(v); // == 12
v = 12.0;
std::get<double>(v); // == 12.0
std::get<1>(v); // == 12.0

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
2
3
4
5
6
7
8
9
10
11
12
13
14
std::optional<std::string> create(bool b) {
if (b) {
return "Godzilla";
} else {
return {};
}
}

create(false).value_or("empty"); // == "empty"
create(true).value(); // == "Godzilla"
// optional-returning factory functions are usable as conditions of while and if
if (auto str = create(true)) {
// ...
}

std::any

A type-safe container for single values of any type.

1
2
3
4
5
std::any x {5};
x.has_value() // == true
std::any_cast<int>(x) // == 5
std::any_cast<int&>(x) = 10;
std::any_cast<int>(x) // == 10

std::string_view

A non-owning reference to a string. Useful for providing an abstraction on top of strings (e.g. for parsing).

1
2
3
4
5
6
7
// Regular strings.
std::string_view cppstr {"foo"};
// Wide strings.
std::wstring_view wcstr_v {L"baz"};
// Character arrays.
char array[3] = {'b', 'a', 'r'};
std::string_view array_v(array, std::size(array));
1
2
3
4
5
std::string str {"   trim me"};
std::string_view v {str};
v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
str; // == " trim me"
v; // == "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
2
3
4
5
std::map<int, string> src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}};
std::map<int, string> dst {{3, "three"}};
dst.insert(src.extract(src.find(1))); // Cheap remove and insert of { 1, "one" } from `src` to `dst`.
dst.insert(src.extract(2)); // Cheap remove and insert of { 2, "two" } from `src` to `dst`.
// dst == { { 1, "one" }, { 2, "two" }, { 3, "three" } };

Inserting an entire set:

1
2
3
4
5
std::set<int> src {1, 3, 5};
std::set<int> dst {2, 4, 5};
dst.merge(src);
// src == { 5 }
// dst == { 1, 2, 3, 4, 5 }

Inserting elements which outlive the container:

1
2
3
4
5
6
auto elementFactory() {
std::set<...> s;
s.emplace(...);
return s.extract(s.begin());
}
s2.insert(elementFactory());

Changing the key of a map element:

1
2
3
4
5
std::map<int, string> m {{1, "one"}, {2, "two"}, {3, "three"}};
auto e = m.extract(2);
e.key() = 4;
m.insert(std::move(e));
// m == { { 1, "one" }, { 3, "three" }, { 4, "two" } }

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
2
3
4
5
std::vector<int> longVector;
// Find element using parallel execution policy
auto result1 = std::find(std::execution::par, std::begin(longVector), std::end(longVector), 2);
// Sort elements using sequential execution policy
auto result2 = std::sort(std::execution::seq, std::begin(longVector), std::end(longVector));