c++ - 这是协程的合理用例吗?
问题描述
我试图了解协程的用例,我想知道这是否是 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
为" b
and ", "-", "" 和 "--"。c
d
我通过协程实现这个 API 有什么收获吗?如果是这样,代码会是什么样子?
解决方案
你的直觉是正确的用协程解决这个问题。文本转换和解析问题是协程的经典应用。事实上,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();
}
推荐阅读
- apache-spark - 大量窗口函数和非常大的数据集上的 Spark 性能问题
- c - 如何在c中递归列出目录的所有文件
- java - PreparedStatements 的正则表达式
- java - 在骆驼路线中实现计数器变量
- angular - Angular Material:当此值为对象时,Mat-select 不会获取所选表单的值
- android - 更改底部导航的图标位置
- mongodb - MongoDB多个集合或多个数据库
- typescript - 根据内部数组的值过滤对象数组
- linux - 仅在标题中带有关键字“mono”的文件上尝试在文件夹中使用 dpkg
- android - 有没有办法在没有活动的情况下使用 Viewpager 实现 TabLayout?