[译]C++20: 协程一瞥
Opened this issue · 4 comments
个别地方翻译的不好,勿怪。
C++20提供了4大特性,刷新了我们对于现代C++编程的认知:概念(concepts)、范围库(the range library)、协程(coroutines)和模块(modules)。关于概念和范围库我已经写了一些博文。现在让我们来看一下协程:
我想用这张图来展开对于协程的深入讨论。
协程是这样一类函数,它能在运行期间挂起和恢复,并且能保存状态。C++中函数的进化步步深入。我在C++20中当成新知识来解读的这一特性其实是非常古老的概念。Melvin Conway发明了术语协程:coroutine。在1963年,在他关于编译器构造的出版物种首先引入了这一概念。Donald Knuth也成过程(procedures)是一种特殊的协程。
在C++20中,新关键字co_await
和co_yield
给C++的函数扩充了两个新概念。
- 通过co_await表达式,使得挂起和恢复一个表达式的执行过程成为可能。如果你在函数func中使用了co_await表达式,然后执行auto getResult = func() ,即使函数的结果尚不可用,也不会阻塞调用。代替了资源紧张的阻塞,你拥有了一个资源友好的等待。
- co_yield表达式允许你写一个生成器函数(generator function)。生成器函数每次都返回一个新值。生成器函数是一种数据流,可以从任意你想获取值地方的获取值。数据流可以是无限的,随之而来的,我们处在一个惰性计算(lazy evaluation)的时代之中。
在我展示一个生成器函数以彰显普通函数和协程的不同之前,我想先谈一谈函数的进化!
函数的进化之路
下面的代码样例一步一步地展示着函数的进化之路:
// functionEvolution.cpp
int func1() {
return 1972;
}
int func2(int arg) {
return arg;
}
double func2(double arg) {
return arg;
}
template <typename T>
T func3(T arg) {
return arg;
}
struct FuncObject4 {
int operator()() { // (1)
return 1998;
}
};
auto func5 = [] {
return 2011;
};
auto func6 = [] (auto arg){
return arg;
};
int main() {
func1(); // 1972
func2(1998); // 1998
func2(1998.0); // 1998.0
func3(1998); // 1998
func3(1998.0); // 1998.0
FuncObject4 func4;
func4(); // 1998
func5(); // 2011
func6(2014); // 2014
func6(2014.0); // 2014
}
- 伴随着1972年,C语言标准的诞生,我们有了普通函数:fun1。
- 1998年C++的标准使得函数变得更加强大。我们拥有了:
- 函数重载:fun2。
- 函数模板:fun3。
- 函数对象:fun4。之前,它被错误的称为仿函数(functors)由于它是operator()重载的产物。
- C++11给了我们lamda函数:fun5。
- C++14,lambda函数拥有了泛型。
让我们迈向未来,看看特殊的协程:生成器。
生成器
在传统C++中,我们可以实现一个贪婪的生成器(greedy generator)。
贪婪生成器
下面的代码简单有效。函数getNumbers返回从begin到end的累加整数。begin小于end并且inc是正数。
// greedyGenerator.cpp
#include <iostream>
#include <vector>
std::vector<int> getNumbers(int begin, int end, int inc = 1) {
std::vector<int> numbers; // (1)
for (int i = begin; i < end; i += inc) {
numbers.push_back(i);
}
return numbers;
}
int main() {
std::cout << std::endl;
const auto numbers= getNumbers(-10, 11);
for (auto n: numbers) std::cout << n << " ";
std::cout << "\n\n";
for (auto n: getNumbers(0, 101, 5)) std::cout << n << " ";
std::cout << "\n\n";
}
当然,我也可以用算法std::iota重新造轮子,这样实现的getNumbers会更好。按下不表。
程序中两次输出是很有必要的。第一行的vector numbers可以得到所有值,尽管我只对这个拥有1000个元素的vector的前5个元素感兴趣。另外将这个函数转型成懒惰生成器十分简单。
懒惰生成器
就这样:
// lazyGenerator.cpp
#include <iostream>
#include <vector>
generator<int> generatorForNumbers(int begin, int inc = 1) {
for (int i = begin;; i += inc) {
co_yield i;
}
}
int main() {
std::cout << std::endl;
const auto numbers= generatorForNumbers(-10); // (2)
for (int i= 1; i <= 20; ++i) std::cout << numbers << " "; // (4)
std::cout << "\n\n";
for (auto n: generatorForNumbers(0, 5)) std::cout << n << " "; // (3)
std::cout << "\n\n";
}
greedyGenerator.cpp的函数getNumbers返回了一个vector。lazyGenerator.cpp中的generatorForNumbers返回了一个生成器。
(2)处的numbers 或者(3)处的 generatorForNumbers(0, 5) 在请求时返回一个新数字。基于范围的循环触发了这一查询请求。说的再清晰一点。协程通过co_yield返回值i 并且理解挂断了运行。如果一个新的值被请求,这个协程将继续执行。
(3)处的表达式generatorForNumbers(0, 5) 仅仅是生成器的原地(in-place)用法。我想明确这一点,因为for循环没有终止条件,generatorForNumbers创造了一个无穷无尽的数据流。如果我仅仅想请求有限个数的值(像(4)处那样),那么无限的数据流没什么问题。而(3)处这种写法,表达式将永远运行下去!
接下来是什么?
我们并没有在C++20中获得完整的协程。但是我们得到了一个可以自己实现协程的框架,你可以假设对于这个话题,我将会有很多内容可以写。
以上为译文
THE END
译者注
std::iota生成器:
#include <iostream>
#include <numeric>
#include <vector>
void iota_test() {
std::vector<int> foo(10);
// 将从 0 开始的 10 次递增值赋值给 foo
std::iota(foo.begin(), foo.end(), 0);
// 输出 foo 中的内容
std::copy(foo.begin(),foo.end(), std::ostream_iterator<int>(std::cout, " "));
std::cout << std::endl;
}
int main() {
iota_test();
}
输出:
0 1 2 3 4 5 6 7 8 9
iota在头文件numeric中。并且这个不是C++11的,之前就有。