首页 > 解决方案 > 这是协程的合理用例吗?

问题描述

我试图了解协程的用例,我想知道这是否是 C++20 协程的合理用例。

我正在编写一个库来处理 UTF-8 字符流中的文本替换。我在想我会有以下类的方法:

std::u8string parse(std::u8string input_string);
std::u8string flush();

在对 so 的调用结束时,一个替换可能处于未完成状态\parse,例如,如果有一个替换,比如说,---到 - 那么一系列调用

auto a = charsub.parse(u8"and --");
auto b = charsub.parse(u8"- ");
auto c = charsub.parse(u8"--");
auto d = charsub.flush();

将 , 和 的值分别初始化a为" band ", "-", "" 和 "--"。cd

我通过协程实现这个 API 有什么收获吗?如果是这样,代码会是什么样子?

标签: c++c++-coroutine

解决方案


你的直觉是正确的用协程解决这个问题。文本转换和解析问题是协程的经典应用。事实上,Conway 用于第一个协程的示例与您的问题非常相似。

每当有一个函数需要跨调用保持状态时,就会想到协程。在 C++ 协程之前,我们可以使用函子或捕获 lambda 来解决此类问题。协程带来的不仅是维护本地数据的状态,还有维护本地逻辑状态的能力。

因此,与在每次调用时检查其入口处的状态的普通函数相比,协程代码可以更简单并且具有更好的流程。

除了在本地保持状态之外,协程还允许函数提供自定义点或钩子,类似于作为回调传入的 lambda。

使用 lambdas 作为回调是控制反转的一个例子。然而,基于协程的设计与控制反转相反——“控制反转”,如果你愿意的话,回到客户端代码来决定何时co_await以及如何处理结果。

例如,下面的协程可以生成单个字符或连续 3 个破折号,而不是产生转换后的字符串 - 然后应用程序代码可以用 em 破折号或其他东西替换 3 个破折号。这产生的“事件”可能是其他字符序列或模式的出现。协程的工作将缩小到扫描输入字符串 - 替换和转换将是另一个协程的责任

我对您的问题的协程解决方案相对简单,并且可能看起来与非协程函数没有太大区别。主要区别在于它在输入中保持破折号的状态,否则需要在外部保留。

它使用最小且直接的 C++ 协程机制——除了一件事。由于您的 parse 函数在每次调用时都会接受新输入,因此我需要以某种方式更新协程本地的输入字符串。这并不简单,可以通过不同的方式完成。

我选择在co_await对本地字符串变量的引用上创建协程。局部变量的引用/地址存储在协程承诺中。这是通过使用协程承诺co_await的方法进行拦截来实现的。await_transform

一旦 promise 有了局部变量的地址,就可以通过公共返回对象对其进行更新。

协程中的局部字符串变量是避免不必要的字符串复制的指针。

这种技术在访问协程中的局部变量时有点笨拙——虽然需要更多的协程代码来代替co_await另一个返回新输入字符串的协程,但这会更好。

我也避免了u8string,因为使用起来很痛苦

该代码使用 gcc 11.2 和 vc++ 2022 版本 17.1 进行了测试

g++ -std=gnu++23 -ggdb3 -O0 -Wall -Werror -Wextra -fcoroutines -o parsedashes parsedashes.cpp
$ parsedashes < <(echo -e "and --\n- \n--")

and
—

--

完整的程序

// parsedashes.cpp

#include <stdio.h>
#include <iostream>
#include <coroutine>
#include <string>

using namespace std;

static void usage()
{
  cout << "usage: parsedashes <in.txt" << "\n";
}

协程返回对象及其公共 API

struct ReplaceDashes {
  struct Promise;
  using promise_type = Promise;
  coroutine_handle<Promise> coro;

  ReplaceDashes(coroutine_handle<Promise> h): coro(h) {}

  ~ReplaceDashes() {
    if(coro)
      coro.destroy();
  }

// resume the suspended coroutine
  bool next() {
    coro.resume();
    return !coro.done();
  }

// return the value yielded by coroutine
  string value() const {
    return coro.promise().output;
  }

// set the input string and run coroutine
  ReplaceDashes& operator()(string* input) {
    *coro.promise().input = input;
    coro.resume();
    return *this;
  }

它的内部承诺对象

  struct Promise {
// address of a pointer to the input string
    string** input;
// the transformed output aka yielded value of the coroutine
    string output;

    ReplaceDashes get_return_object() {
      return ReplaceDashes{coroutine_handle<Promise>::from_promise(*this)};
    }

// run coroutine immediately to first co_await
    suspend_never initial_suspend() noexcept {
      return {};
    }

// set yielded value to return
    suspend_always yield_value(string value) {
      output = value;
      return {};
    }
// set returned value to return
    void return_value(string value) {
      output = value;
    }

    suspend_always final_suspend() noexcept {
      return {};
    }

    void unhandled_exception() noexcept {}
// intercept co_await on the address of the local variable in
// the coroutine that points to the input string
    suspend_always await_transform(string** localInput) {
      input = localInput;
      return {};
    }

  };

};

实际的协程函数

ReplaceDashes replaceDashes()
{
  string dashes;
  string outstr;

// input is a pointer to a string instead of a string
// this way input string can be changed cheaply
  string* input{};

// pass a reference to local input string to keep in coroutine promise
// this way input string can be set from outside coroutine
  co_await &input;

  for(unsigned i = 0;;) {
    char chr = (*input)[i++];
// string is consumed, return the transformed string
// or any leftover dashes if this was the final input
    if(chr == '\0') {
      if(i == 1) {
        co_return dashes;
      }
      co_yield outstr;
// resume to process new input string
      i = 0;
      outstr.clear();
      continue;
    }
// append non-dash after any accumulated dashes
    if(chr != '-') {
      outstr += dashes;
      outstr += chr;
      dashes.clear();
      continue;
    }
// accumulate dashes
    if(dashes.length() < 2) {
      dashes += chr;
      continue;
    }
// replace 3 dashes in a row
// unicode em dash u+2014 '—' is utf8 e2 80 94
    outstr += "\xe2\x80\x94";
    dashes.clear();
  }

}

解析器 API

struct Charsub {

  ReplaceDashes replacer = replaceDashes();

  string parse(string& input) {
    return replacer(&input).value();
  }

  string flush() {
    replacer.next();
    return replacer.value();
  }

};

驱动程序

int main(int argc, char* argv[])
{
  (void)argv;

  if(argc > 1) {
    usage();
    return 1;
  }

  Charsub charsub;

  for(string line; getline(cin, line);) {
    cout << charsub.parse(line) << "\n";
  }
  cout << charsub.flush();

}

推荐阅读