与C++20中简单的有状态lambda相比,`co_yield`的特殊值是什么?

hgc7kmma  于 2023-05-02  发布在  其他
关注(0)|答案(2)|浏览(111)

来自著名的C++协程库(搜索“Don't allow any use of co_await inside the generator coroutine.“在源文件generator.hpp中),并且从我自己的实验中,我知道使用co_yield的协程不能同时使用co_await
由于使用co_yield的生成器必须是同步的,那么,使用co_yield比简单的有状态lambda有什么优势呢?
例如:

#include <iostream>

generator<int> g()
{
    for (auto i = 0; i < 9; ++i)
    {
        co_yield i;
    }
}

int main()
{
    auto fn_gen = [i = 0] mutable { return i++; };

    // Lambda way
    for (auto i = 0; i < 9; ++i)
    {
        std::cout << fn_gen() << std::endl;
    }

    // co_yield way
    for (auto i : g())
    {
        std::cout << i << std::endl;
    }
}

与C++20中简单的有状态lambda相比,co_yield的特殊值是什么?
请参见更新的MWEhttps://godbolt.org/z/x1Yoen7Ys

在更新的示例中,当在同一协程中使用co_awaitco_yield时,输出完全出乎意料。

ego6inou

ego6inou1#

对于具有最小内部状态和代码的平凡生成器,一个小的函子或lambda就可以了。但是,随着生成器代码变得越来越复杂,需要更多的状态,它就变得不那么精细了。你必须在你的函子类型或lambda说明符中加入更多的成员。你在函数内部有越来越大的代码。等等
在最极端的情况下,一个基于co_yield的生成器可以对外界隐藏 * 所有 * 实现细节,只需将其定义放在。有状态函子不能隐藏其内部状态,因为其状态是外部世界必须看到的类型的成员。避免这种情况的唯一方法是通过类型擦除,例如使用std::function。在这一点上,您基本上没有获得任何超过使用co_yield的好处。
此外,co_await可以与co_yield一起使用。Cppcoro的generator类型显式地支持它,但cppcoro不是C++20。您可以编写任何想要的生成器,并且该生成器可以支持将co_await用于特定目的。
实际上,您可以创建异步生成器,有时可以立即生成值,有时可以通过一些异步进程来调度值的可用性。调用异步生成器的代码可以对它执行co_await操作以从中提取值,而不是像对待函子或迭代器对那样对待它。

vwoqyblh

vwoqyblh2#

有状态lambda或自定义函子几乎总是更好的选择。事实上,你可以通过使用lambda表达式来获得更高效的协程。比较一下:
演示

#include <cstdio>
#include <cstdint>

int main() {

    enum class cont_point : uint8_t {
        init,
        first,
        second,
        third,
        end,
    };

    auto lambda = [cp = cont_point::init]() mutable -> void {
        switch(cp) {
            case cont_point::init:
                printf("init\n");
                cp = cont_point::first;
                break;
            case cont_point::first:
                printf("first\n");
                cp = cont_point::second;
                break;
            case cont_point::second:
                printf("second\n");
                cp = cont_point::third;
                break;
            case cont_point::third:
                printf("third\n");
                cp = cont_point::end;
                break;
            default:
                return ;
        }
    };
    
    lambda();
    lambda();
    lambda();
    lambda();
}

产量:

init
first
second
third

如果你检查程序集,你会看到代码被优化到完美,这给了你一个关于编译器在优化lambda表达式方面有多高效的提示。对于协程来说,情况并非如此(至少目前还不是)。

但是

协程提供了一个非常有趣的利基案例,没有其他语言结构可以填补,即它们解决了cactus stack problem。仙人掌堆栈问题基本上是指代码分叉在同一个堆栈上运行的问题-这是不可能的,所以必须生成一个单独的堆栈。如果该堆栈上的执行线程再次分叉,则必须有另一个堆栈,依此类推。更糟糕的是,没有人知道这些堆栈会有多大。
C++20协程是无栈的,这反过来意味着它们确实使用堆栈,但不是用于有状态数据,只有没有遍历可等待点的数据才会被抛出到执行任务的堆栈上,因此它可以在堆栈展开期间安全地删除,而所有有状态数据都保留在称为协程帧的东西上,通常(不幸的是,即使在简单优化的情况下)依赖于堆(通过operator new分配)。在执行过程中,将什么放入协程框架以及将什么放入调用堆栈的决定由编译器在一个名为 * 协程转换 * 的过程中完成。正是这个过程使得协程能够独特地解决仙人掌堆栈问题,如下所示:
每个新分配的协程示例将在堆上保留预定义的空间量,相当于具有数据字段的对象。当执行协程时,附加的数据被放到正在执行协程的延续的任何任务的堆栈上。通过这种方式,堆栈可以动态地增长,而我们没有许多堆栈溢出的问题(就像堆栈式协程的情况一样),但我们只需要确保所有线程都有足够的堆栈空间。

相关问题