百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

聊聊C++20最大的变革之一 —— Coroutine,看不懂你打我(三)

liebian365 2025-02-06 15:36 18 浏览 0 评论

截止到上一篇文章我们全部介绍完了C++ coroutine相关的概念,这一篇文章作为整个系列的第3篇将实现一个通用的 Generator 类型方便以及展示如何通过 Generator 向coroutine内传递数据的方法。这一章里不会有新的关于Coroutine的概念,所以我会比较详细的解释类型设计的方法和容易出错的地方,不会细致讲解每一行代码。

Generic Generator

这一段代码开始我们将大量使用模版(毕竟是C++的精髓),如果大家对模板不是特别熟悉可以找一找相关的教程。接下来我们首先看一个简单的通用Generator模版类的实现:

#include 
#include 
#include 
#include 

template 
class Generator {
 public:
  class promise_type {
   public:
    Generator get_return_object() { return Generator(std::coroutine_handle::from_promise(*this)); }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { exception_ = std::current_exception(); }
    void return_void() {}

    template  From>  // C++20 concept
    std::suspend_always yield_value(From&& value) {
      value_ = std::forward(value);
      return {};
    }

    T value_;
    std::exception_ptr exception_;
  };

  Generator(const std::coroutine_handle& handle) : handle_(handle) {}

  ~Generator() { handle_.destroy(); }

  explicit operator bool() {
    Next();
    return !handle_.done();
  }

  const T& operator()() {
    Next();
    consumed_ = true;
    return handle_.promise().value_;
  }

 private:
  void Next() {
    if (consumed_) {
      handle_();
      if (handle_.promise().exception_) {
        std::rethrow_exception(handle_.promise().exception_);
      }
      consumed_ = false;
    }
  }

  bool consumed_ = true;
  std::coroutine_handle handle_;
};

/*

The real logic implementation, looks great even we've wrote a lot of clunky code above...

*/

Generator counter(size_t num) {
  for (size_t i = 0; i < num; ++i) {
    co_yield i;
  }
}

int main() {
  auto gen = counter(3);
  while (gen) {
    std::cout << "main:" << gen() << std::endl;
  }
  return 0;
}

/*
Outputs:
main:0
main:1
main:2
*/

这段代码6 - 55行是 Generator 的实现,63 - 75行是使用方法,我们可以看到最终实际使用时的代码非常简单。这里要稍微讲解一下的是:

  1. Generator的 operator bool() 以及 operator()() 都会尝试继续coroutine的执行以便于确定是否还有新的值。
  2. Generator的 operator bool() 以及 operator()() 都有可能抛出异常,我个人感觉这样的设计是不好的。因为一般来说,一个“判断Generator是否有值”这样的操作不太应该发生错误,异常只应该在读取这个值的时候发生。不过这个实现其实主要抄袭的 cppreference 上给出的例子,所以我也没改。

这段代码很简单,没啥要解释的,有疑问的朋友可以在评论里留下问题。

接下来我们看本篇文章中主要想介绍的“升级强化版Generator”,主要是新实现两个功能:

  1. counter可以接收main发送给它的信息(类似python generator.send)
  2. main可以通过迭代器接口来读取generator的数据

接下来我们直接看代码,然后讲解关键点:

#include 
#include 
#include 
#include 
#include 

template 
class Generator {
 public:
  //
  // Promise
  //
  class promise_type {
   public:
    Generator get_return_object() { return Generator(std::coroutine_handle::from_promise(*this)); }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { exception_ = std::current_exception(); }
    void return_void() {}

    template  From>  // C++20 concept
    auto yield_value(From&& value) {
      return_value_ = std::forward(value);

      struct send_awaiter {
        std::coroutine_handle handle_;

        constexpr bool await_ready() const noexcept { return false; }
        void await_suspend(std::coroutine_handle<> h) {}
        const SendType& await_resume() const noexcept {
          // Return the send value
          return handle_.promise().send_value_;
        }
      };

      return send_awaiter{std::coroutine_handle::from_promise(*this)};
    }

    SendType send_value_;
    ReturnType return_value_;
    std::exception_ptr exception_;
  };

  //
  // Iterator
  //

  class sentinel {};

  class iterator {
   public:
    using iterator_category = std::input_iterator_tag;
    using difference_type = std::ptrdiff_t;
    using value_type = ReturnType;
    using reference = ReturnType&;
    using pointer = ReturnType*;

    template 
    iterator(Generator& gen, T&& send_value) noexcept : gen_(gen), send_value_(std::forward(send_value)) {
      operator++();  // Initial read
    }
    iterator(const iterator&) = default;
    ~iterator() = default;

    friend bool operator==(const iterator& it, sentinel) noexcept {
      // The iterator stopped when generator is done
      return it.gen_.Done();
    }

    iterator& operator++() {
      gen_.Next(send_value_);
      return *this;
    }

    void operator++(int) { operator++(); }

    reference operator*() const { return gen_.Get(); }

   private:
    Generator& gen_;
    SendType send_value_;
  };

  //
  // Generator
  //

  Generator(const std::coroutine_handle& handle) : handle_(handle) {}

  ~Generator() { handle_.destroy(); }

  explicit operator bool() const noexcept { return !handle_.done() && !consumed_; }

  ReturnType& Get() {
    consumed_ = true;
    if (exception_) {
      std::rethrow_exception(exception_);
    }
    return *value_;
  }

  bool Next() { return Next(DEFAULT_SEND_VALUE); }

  template 
  bool Next(T&& send_value) {
    if (!handle_.done()) {
      handle_.promise().send_value_ = std::forward(send_value);
      handle_();
      if (exception_ = handle_.promise().exception_; exception_) {
        // Has exception, and should return true as if there's new value (To let the exception rethrow by Get)
        consumed_ = false;
        value_ = {};
        return true;
      } else if (handle_.done()) {
        // The final suspend, no more to read
        consumed_ = true;
        value_ = {};
        return false;
      } else {
        // Has new value
        consumed_ = false;
        value_ = std::move(handle_.promise().return_value_);  // Move the value out of promise
        return true;
      }
    } else {
      // Done, no more to read
      consumed_ = true;
      value_ = {};
      return false;
    }
  }

  bool Done() const { return handle_.done(); }

  iterator begin() { return begin(DEFAULT_SEND_VALUE); }

  iterator begin(SendType&& send_value) {
    // Create iterator by custom send value
    return iterator(*this, std::forward(send_value));
  }

  sentinel end() noexcept { return {}; }

 private:
  std::coroutine_handle handle_;
  bool consumed_ = true;
  std::optional value_;
  std::exception_ptr exception_;
};

template 
Generator counter(size_t max) {
  for (size_t i = 0; i < max;) {
    auto step = co_yield i;
    i += step;
  }
}

int main() {
  //
  // Usage 1
  //
  std::cout << "[+] Usage1" << std::endl;
  auto gen1 = counter(5);
  while (gen1.Next(2)) {
    std::cout << "main:" << gen1.Get() << std::endl;
  }
  //
  // Usage 2
  //
  std::cout << "[+] Usage2" << std::endl;
  auto gen2 = counter(9);
  for (auto it = gen2.begin(3); it != gen2.end(); ++it) {
    std::cout << "main:" << *it << std::endl;
  }
  //
  // Usage 3
  //
  std::cout << "[+] Usage3" << std::endl;
  auto gen3 = counter(3);
  for (const auto value : gen3) {
    std::cout << "main:" << value << std::endl;
  }
  return 0;
}

/*
Outputs:
[+] Usage1
main:0
main:2
main:4
[+] Usage2
main:0
main:3
main:6
[+] Usage3
main:0
main:1
main:2
*/

我们首先看 counter 函数,主要逻辑是不变的但是变量i的自增逻辑发生了变化,for循环每次不再默认i+1,而是通过yield返回的step来进行自增。在main函数中我们展示了3种用法:

  1. 使用while循环配合 Next / Get 方法获得 Generator 返回的值,通过Next方法的参数把step值传递给counter函数
  2. 使用for循环配合迭代器获得 Generator 返回的值,通过begin方法的参数设置迭代过程中默认传递的值
  3. 使用for range循环,采用默认参数的begin方法

上面的代码逻辑上很简单,我们首先看main函数是如何通过 Next 方法把值传递给 counter 方法的。首先回忆一下 co_yield 关键字:

// co_yield a;等价于
co_await promise.yield_value(a);
// 那么 auto r = co_yield a;等价于
auto r = co_await promise.yield_value(a);

我们看最后一行就非常容易理解 co_yield 关键字的返回值了,实际上就是 yield_value 所返回的awaiter对象的 await_resume 方法的返回值了。所以我们来看21 - 37行 yield_value 实现代码,其返回了一个匿名的awaiter类型,这个类型只干了两件事:

  1. 让coroutine暂停在 co_yield 的位置
  2. co_yield 返回 promise.send_value_ 的值

接下来我们看 send_value_ 是在哪里被写入的,我们来看107行 Next 函数的实现,在恢复counter执行之前修改 promise.send_value_。

接下来我们看迭代器的实现,这一部分就非常简单没有什么奇淫巧技(如果有对iterator不太熟悉的朋友,可以找一找这方面的教程),这里面有几个要强调的是:

  1. 在Next被调用之前, Generator 是没有任何有效返回值的( promise.initial_suspend 暂停住了),因此 iterator 的构造函数中需要执行一次迭代操作。
  2. iterator 的终止条件就是 Generator 已经执行完毕了。

这段代码也没什么要解释的了,有疑问的朋友可以在评论里留下问题。

本文到此就结束了,因为这一篇文章没有引入任何C++ coroutine的新概念因此解释的非常快,只对 Generator 实现的关键点做了一些讲解。大家有看不懂、不理解的地方可以在评论里留下问题,我会逐个回复。下一篇也就是本系列的最后一篇,我们将实现一个简单的“异步运行时框架”,欢迎大家继续关注~

喜欢就点个赞、收个藏、关个注吧!

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: