Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[译]C++20: 协程一瞥 #31

Open
guodongxiaren opened this issue Mar 30, 2020 · 4 comments
Open

[译]C++20: 协程一瞥 #31

guodongxiaren opened this issue Mar 30, 2020 · 4 comments
Labels
C/C++ 兵中凶兽 C++ 翻译的国外好文

Comments

@guodongxiaren
Copy link
Owner

guodongxiaren commented Mar 30, 2020

原文: http://www.modernescpp.com/index.php/c-20-coroutines-the-first-overview#h6-1-2-c-standard-library-including-c-14-amp-c-17

个别地方翻译的不好,勿怪。

C++20提供了4大特性,刷新了我们对于现代C++编程的认知:概念(concepts)、范围库(the range library)、协程(coroutines)和模块(modules)。关于概念和范围库我已经写了一些博文。现在让我们来看一下协程:
image
我想用这张图来展开对于协程的深入讨论。

协程是这样一类函数,它能在运行期间挂起和恢复,并且能保存状态。C++中函数的进化步步深入。我在C++20中当成新知识来解读的这一特性其实是非常古老的概念。Melvin Conway发明了术语协程:coroutine。在1963年,在他关于编译器构造的出版物种首先引入了这一概念。Donald Knuth也成过程(procedures)是一种特殊的协程。

在C++20中,新关键字co_awaitco_yield给C++的函数扩充了两个新概念。

  • 通过co_await表达式,使得挂起和恢复一个表达式的执行过程成为可能。如果你在函数func中使用了co_await表达式,然后执行auto getResult = func() ,即使函数的结果尚不可用,也不会阻塞调用。代替了资源紧张的阻塞,你拥有了一个资源友好的等待。
  • co_yield表达式允许你写一个生成器函数(generator function)。生成器函数每次都返回一个新值。生成器函数是一种数据流,可以从任意你想获取值地方的获取值。数据流可以是无限的,随之而来的,我们处在一个惰性计算(lazy evaluation)的时代之中。

在我展示一个生成器函数以彰显普通函数和协程的不同之前,我想先谈一谈函数的进化!

@guodongxiaren guodongxiaren added C/C++ 兵中凶兽 C++ 翻译的国外好文 labels Mar 30, 2020
@guodongxiaren
Copy link
Owner Author

函数的进化之路

下面的代码样例一步一步地展示着函数的进化之路:

// 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函数拥有了泛型。

让我们迈向未来,看看特殊的协程:生成器。

@guodongxiaren
Copy link
Owner Author

生成器

在传统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";

}

程序的输出符合预期:
image

当然,我也可以用算法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)处这种写法,表达式将永远运行下去!

@guodongxiaren
Copy link
Owner Author

guodongxiaren commented Mar 30, 2020

接下来是什么?

我们并没有在C++20中获得完整的协程。但是我们得到了一个可以自己实现协程的框架,你可以假设对于这个话题,我将会有很多内容可以写。


以上为译文

THE END


@guodongxiaren
Copy link
Owner Author

译者注

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的,之前就有。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C/C++ 兵中凶兽 C++ 翻译的国外好文
Projects
None yet
Development

No branches or pull requests

1 participant