C++20:四大件
C++20 (C++ 编程语言标准 2020 版) 将是 C++ 语言一次非常重大的更新,将为这门语言引入大量新特性。近日,C++ 开发者 Rainer Grimm 正通过一系列博客文章介绍 C++20 的新特性。笔者不才,将其翻译,以供参考和学习。
原文详见: C++20: The Big Four
这篇文章将向你介绍 C++20 的四大件(四大特性):概念(concepts)、范围(ranges)、协程(coroutines)以及模块(modules)。C++20 提供了很多东西。在介绍四大件之前,让我们先对 C++20 有个概览。因为除了四大件之外,C++20 中还有许多将会对核心语言、程序库以及并发能力带来深远影响的新特性。
C++20 的编译器支持情况
学习新特性最简单的方法就是去使用它们。那么,立刻便会产生这个问题:哪些 C++20 的特性已经被哪些编译器支持了呢?像往常一样,网站 compiler_support 已经给你提供了这个问题的答案。
简单来说,全新的 GCC、Clang 和 EDG 编译器为核心语言提供了最好的支持。此外,MSVC 和 Apple Clang 编译器也支持许多 C++20 特性。 标准库的情况也类似。GCC 对标准库的支持最好,其次是 Clang 和 MSVC 编译器。 以上截图只显示了表格的开头,即便这样它们也给出了一个不太令人满意的答案。即使你使用所有全新的编译器,仍然有许多编译器不支持的特性。通常,你会找到变通方法来使用新特性。这里举两个例子:
- 概念(Concepts): GCC 支持一个概念的早期版本。
- std::jthread:在 Github 上有一个由 Nicolai Josuttis 维护的实现草案。
简而言之,情况没那么糟。只需稍加修改,就可以尝试许多新特性。
那么现在,让我来给你一个新特性的鸟瞰图吧。当然,我们还是应该从四大件开始。
四大件(The Big Four)
概念(Concepts)
使用模板进行泛型编程的关键想法是可以定义用于各种类型的函数和类。但是,在实例化模板时经常会出现用错类型的问题,编译器这时就会“喷出”“成吨的”报错信息。
现在概念来了,这个问题可以休矣。概念(concept)使你能够为模板编写一个可以由编译器来检查的约束(requires)。概念将彻底改变我们思考和编写泛型代码的方式。因为:
- 约束(requires)将变为模板接口的一部分而存在
- 函数的重载或类模板的特化可以基于概念
- 因为编译器能够将模板参数的约束与实际模板参数相比较,从而使我们能得到更加友好的报错信息
然而,这还不是全部。
- 你可以使用预定义的概念或定义自己的概念
- auto 和概念的用法统一到了一起。你可以不使用 auto,而是使用概念
- 如果函数声明使用了概念,它将自动成为函数模板。因此,编写函数模板与将与编写函数一样容易
以下代码片段向你展示了一个简单概念 Integral 的定义和用法:
template<typename T>
concept bool Integral(){
return std::is_integral<T>::value;
}
Integral auto gcd(Integral auto a,
Integral auto b){
if( b == 0 ) return a;
else return gcd(b, a % b);
}
Integral 是一个概念,它要求类型参数 T 必须满足 std::is_integral<T>::value 的要求。std::is_integral<T>::value 是一个来自于类型萃取库(type-traits library)的函数,它能够在编译期检查 T 是否为整数类型。如果 std::is_integral<T>::value 的结果为 true,则一切都好;反之,你将会得到一个编译期错误。如果你对类型萃取感兴趣的话,请移步至我的关于 type-traits library 的系列文章。
以上 gcd 函数是使用欧几里得算法来计算最大公约数(greatest common divisor)的。在定义 gcd 时我使用了函数模板的缩写语法。gcd 是一个对参数和返回值都有约束的函数模板,它求其参数和返回类型支持概念 Integral。当我删除这个语法糖时,你也许可以看出 gcd 的本质。
以下是语义上等价的 gcd 算法:
template<typename T>
requires Integral<T>()
T gcd(T a, T b){
if( b == 0 ) return a;
else return gcd(b, a % b);
}
至此,如果你还没有看到 gcd 的本质,不用担心,我会在之后发布的文章里对概念进行详细介绍的。
范围库(Ranges Library)
范围库是概念的首个用户。它使算法能够:
- 直接对容器进行操作,而不需要迭代器来指定范围
- 支持惰性求值
- 支持组合使用
简而言之:范围库能够支持函数式编程。
多说无益,上代码!下面的代码展示了用管道符对函数进行组合的操作。
#include <vector>
#include <ranges>
#include <iostream>
int main()
{
std::vector<int> ints{0, 1, 2, 3, 4, 5};
auto even = [](int i){ return 0 == i % 2; };
auto square = [](int i) { return i * i; };
for (int i : ints | std::view::filter(even) |
std::view::transform(square)) {
std::cout << i << ' '; // 0 4 16
}
}
even 是一个匿名(lambda)函数,它能够判断传入的 i 是否为偶数(even);而 square 函数则将 i 映射为它的平方(square)。之后则是由左向右的函数组合:for (int i : ints | std::view::filter(even) | std::view::transform(square))。让 ints 中的每个元素通过偶数(even)过滤器(filter)然后将剩余的元素转换(transform)成它的平方(square)。如果你熟悉函数式编程,会发现这读起来就像散文一样优美。
协程(Coroutines)
协程是一种广义的函数,它能在保持内部状态的同时支持暂停和恢复。协程是编写事件驱动应用程序的常用方法。事件驱动的应用程序可以是仿真、游戏、服务器、用户界面,甚至是算法。协程通常也用于协作式多任务处理。
在 C++20 中我们不需要编写具体的协程,我们将得到一个框架来编写自己的协程。这个框架由 20 多个函数组成,你必须部分实现这些函数,而部分函数可能会被覆盖。因此,你可以根据需要定制你的协程。
让我们来看一个特定协程的用法。以下程序使用生成器来生成一个无限的数据流:
Generator<int> getNext(int start = 0, int step = 1)
{
auto value = start;
for (int i = 0;; ++i){
co_yield value; // 1
value += step;
}
}
int main()
{
std::cout << std::endl;
std::cout << "getNext():";
auto gen = getNext();
for (int i = 0; i <= 10; ++i) {
gen.next(); // 2
std::cout << " " << gen.getValue();
}
std::cout << "\n\n";
std::cout << "getNext(100, -10):";
auto gen2 = getNext(100, -10);
for (int i = 0; i <= 20; ++i) {
gen2.next(); // 3
std::cout << " " << gen2.getValue();
}
std::cout << std::endl;
}
好的,让我来补充几句吧。这段程序只是一个代码片段。函数 getNext 是一个协程,因为它使用了关键字 co_yield。getNext 中有一个无限循环,它会在 co_yield 之后返回 value。调用 next()(注释 2、3 所在行)会继续这个协程,接下来的 getValue 调用会获取这个值。在 getNext 被调用之后,协程将被暂停,直到下一次 next() 被调用时。在上例中有一个很大的未知,即 getNext 函数的返回值 Generator<int>。这部分内容很复杂,后面我在写协程的文章中会更详细地介绍。
得益于在线编译器 Wandbox 的使用,我可以向你展示这个程序的输出:
模块(Modules)
模块部分简单介绍一下就好,因为本帖已经足够长了。
模块承诺能够实现:
- 更快的编译时间
- 对宏的隔离
- 表达代码的逻辑结构
- 替代原始的头文件
- 提供一种摆脱丑陋的宏的通用方法
接下来?
在概览了 C++20 四大件之后,我将在下一篇文章中介绍开篇图像中所展示的核心语言特性。