作为 C++20 协程实现的终端命令

dar*_*ght 0 c++ coroutine

我正在用现代C++20编写我的项目。这个应用程序就像图形模式下的终端/控制台,您可以在其中输入带有各自参数的命令,终端将识别第一个单词,如果在其内部映射中找到它,它会调用映射函数(命令本身) 。这很好用。但现在,我意识到有些命令需要用户交互。例如,如果我想从MS-DOS重新创建del命令,并且用户键入,该命令将需要从键盘获取一个字符(表示是,表示否)。其他一些命令将要求用户键入整个字符串。我怎样才能做到这一点?我正在考虑将我的命令实现为协程,这样我就可以暂停执行,让终端知道我想要单个字符或完整字符串,然后恢复。但我发现协程有点复杂。到目前为止,我有这段代码del *.bak /pyn

enum class e_command_input { SINGLE_CHAR, STRING };

class OS_Command
{
    public:

        // Forward declaration
        struct promise_type;

        using t_handle = std::coroutine_handle<promise_type>;

        struct promise_type
        {
            // Creates the command
            auto get_return_object() -> OS_Command
            {
                return OS_Command{ t_handle::from_promise(*this) };
            }

            auto initial_suspend() -> std::suspend_always
            {
                cout << "    Command starts running" << endl;
                return {};
            }

            auto final_suspend() noexcept -> std::suspend_always
            {
                cout << "    Command finishes running" << endl;
                return {};
            }

            void return_void()
            {
            }

            void unhandled_exception()
            {
                m_except = std::current_exception();
            }


            // data
            std::exception_ptr m_except{};
        };


    public:

        // ctor
        OS_Command(t_handle handle)
            : m_handle(handle)
        {
        }

        // Cannot be copyed
        OS_Command(OS_Command&) = delete;

        // dtor
        ~OS_Command()
        {
            if (m_handle)
                m_handle.destroy();
        }


        // Manually start
        void start()
        {
            cout << "Coro start() called" << endl;
            m_handle.resume();
        }

    private:

        // Access to the state of the coroutine
        t_handle m_handle;

};


// Coroutine "factory"? that returns a coroutine object
auto make_command() -> OS_Command
{
    // Is THIS the real coroutine???
    co_await std::suspend_never{};
}


//
int main()
{
    cout << "main: Before command" << endl;

    auto cmd1 = make_command();
    cmd1.start();

    cout << "main: After command" << endl;

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

代码可以编译并运行,但我不明白:

  • 我应该在哪里实现该命令的代码?co_yield 和 co_return 语句应该写在哪里?
  • 如何将枚举传递给我的终端类(调用者),以及如何从终端返回用户输入的 std::string 或字符?
  • make_command()是这里真正的协程吗?这是一家工厂吗?

Yak*_*ont 5

因此,C++ 语言级协程是构建块,可让您构建其他语言直接提供的异步库。

理论上,这可以让您用新的异步习惯用法来扩展,而不仅仅是使用该语言提供的预定义的习惯用法。

想象一下,如果您愿意,C++ 不提供继承和虚函数等,而是提供一种语言,您可以在其中定义您自己的继承和消息传递模型。这会更强大,但当您想要做的只是拥有一个带有可以继承和实现的方法的接口时,这确实很烦人。

在这种情况下,C++ 为您提供了钩子来定义 co_yield、co_await 和类似的含义。您可以使用 C++ 协程系统来发明异步选项,但带有侧通道等异常,并且每个 co_await 都会绕过表达式以 nullopt 提前退出,因此{ return co_await a + co_await b; }重写为{if(!a) return nullopt; if(!b) return nullopt; return *a+*b; }

这种程度的灵活性意味着如果您想编写自己的协程类型,则需要完成大量框架工作。

但一切并没有失去。

您可以尝试找到一个已经编写好的协程来执行您想要的操作。

但如果你想自己写一个,你应该首先退一步思考函数。

函数是一个东西。您可以在函数内部和函数外部对函数执行操作。

从函数外部,您可以向其传递参数,向其传递一个位置以输出返回值,然后执行它。从函数内部,您可以获取传递的参数,并且可以设置返回值,并且可以退出该函数。

现在,我们将这些操作捆绑在 C++ 中 -R x = foo(a,b,c);传递参数、传递返回值并在一条语句中开始执行。但是,您可以轻松想象通过多个步骤来完成此操作。

类似地,在函数内部,我们要求它首先获取参数,并在退出之前立即设置返回值。

C++ 将函数的参数和返回值存储在线程本地堆栈上。但在一种语言中,将存储参数的操作与在堆或数据结构中返回数据的操作分开是合理的。

您可以使用延迟启动策略的 std async 来实现这一点 - 参数不存储在线程本地堆栈中,而是聚集起来并等待它们的使用。在您选择运行该函数之前,甚至返回值也不会存储在堆栈中。

协程是具有更多操作的函数。

当 yiu 第一次调用协程时,它的行为可能有点像工厂。没必要!但如果你打算与它进行交互,它可能会的。

因此,大多数协程的初始函数调用有点像调用sts::async(std::launch_policy::dederred, - 您正在设置工作,但尚未执行。

一旦你安排好了工作,你就可以开始工作了。这可能包括从另一个协程执行 co_await,或者执行handle.resume(). 将从首次调用协程时返回的协程对象的公共方法调用 cresume。

为了允许 co_await 你必须是可等待的 - 有一个完整的合同。

当您第一次调用协程时,会发生一系列与提供的类发生反应的自动步骤。创建一个协程句柄并将其传递给您提供的类,并且设置了一些东西。最终运行初始挂起 - 在这里,系统会询问您是否要立即返回调用者,或者是否要在协程主体中运行 cose。

我建议您首先暂停处理您的问题。然后你的协程就会像你说的那样像工厂一样工作。

调用者现在已经创建了您的“进程”对象。现在应该恢复它并看看会产生什么结果。它可能是输入请求,也可能是进程终止。

在协程主体内,您将使用 co_return 来完成流程,并使用 co_yield 来从 shell 请求某些服务。或者,shell 可以传入一个可等待的服务对象,并且进程协程可以 co_await 来自它的输入;这反过来又会返回到 shell。

在句柄上调用resume的代码负责将任何信息传送回调用者。当协程 co_yields 或 co_await 时,它会以特定方式与协程对象交互,让您有机会复制返回值或其他内容。然后在简历后面的代码中,您可以弄清楚您隐藏了哪些数据,如果它是返回或收益率,则 abd 将其传回给被调用者。

Process p = CreateProc("command");
if(p.syntaxError()) {
  // blah
  return;
}
while (auto x = p.step()){
  query_user(x);
}
print(p.errcode());
Run Code Online (Sandbox Code Playgroud)

并在协程内:

Process CreateProc(string s){
  auto parsed = parse_cmd(s);
  co_yield parse.success();
  // do stuff
  co_yield io_request;
  // do stuff
  co_yield io_request;
}
Run Code Online (Sandbox Code Playgroud)

但用 co_await 来做这件事更有趣:

Process CreateProc(string s, Io io){
  // ...
  x = co_await io.get("%d");
}
Run Code Online (Sandbox Code Playgroud)

因为co_await可以挂起协程并在协程体内返回一个值,这更接近你想要的。

现在,您将遇到的一个问题是从协程调用的函数无法挂起其调用协程。这意味着协程中的可挂起工作也必须是协程。

C++ 协程是无堆栈的。

您的协程可以等待其他协程,并最终到达一个等待 io 的协程,该协程将所有内容挂起回到正在创建的原始进程以执行 io 工作,然后恢复整个链以传递用户输入。

但在某些时候你必须问为什么。为什么不直接在调用堆栈深处处理 io?