C++中与C# Yield相当的语法是什么?

47
public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

是否有一种使用模板技巧(或其他方式)在C++中获得相同语法的方法?


Raymond Chen在http://blogs.msdn.com/b/oldnewthing/archive/2008/08/12/8849519.aspx中分解了`yield`在幕后的工作原理。 - Bill
@Bill 上面的链接无法访问。 - Michael IV
@MichaelIV 微软每隔5年左右迁移他们的博客,这会导致旧链接失效。请查看以下链接:https://devblogs.microsoft.com/oldnewthing/20080812-00/?p=21273 - Bill
11个回答

31

2
+1,这真的很有趣,我不太清楚self.exit()如何成为return语句的合法替代品。(我怀疑这是对异常或longjmp的可怕滥用,但我不确定我是否想知道!) - Flexo
4
Boost.Coroutine 是用汇编实现的,在支持“纤程”(Fibers)的平台上通过操作系统调用运行。它不是用纯 C++ 实现的。 - Mankarse
我在官方网站的boost库列表中没有看到协程。有什么指导吗? - Noah Watkins
12
如果这是针对Win32的,那么请务必了解,在任何代码中使用Fiber都是一个非常高级的话题,看到那些有效地隐藏Fiber代码的库真的很可怕。在存在Fiber的情况下,有许多Win32 API无法正常工作,或更加令人恐惧的是不会按预期工作。例如,Win32中的锁基于线程ID - 这意味着对于Fiber,如果您获取一个锁,然后放弃,同样在您的线程上运行的另一个Fiber也可以成功地获取该锁!因此,除非您非常小心,否则它可能会给您带来麻烦。 - Mike Vine

15
你总是可以手写这个。说实话,对我来说,yield 真的像是在掩饰什么(协程也是一样)。
协程到底是什么?它实际上是一些状态捆绑在一起:
- 一个用于创建协程的函数(难道不是构造函数吗?) - 一个函数用于将其移到下一个状态(传统上不是操作符++吗?)
在 C++ 中,它被称为输入迭代器,并且可以任意增大。
所以,虽然语法可能不那么漂亮,但只需使用标准库即可实现此功能。
static std::array<int, 6> const Array = {{1, 2, 4, 8, 16, 16777216}};

class Integers: public std::iterator<std::input_iterator_tag,
                                      int, ptrdiff_t, int const*, int>
{
public:
  Integers(): _index(0) {}

  operator bool() const { return _index < Array.size(); }

  Integers& operator++() { assert(*this); ++_index; return *this; }
  Integers operator++(int) { Integers tmp = *this; ++*this; return tmp; }

  int operator*() const { assert(*this); return Array[_index]; }
  int const* operator->() const { assert(*this); return &Array[_index]; }

private:
  size_t _index;
}; // class Integers

显然,由于可以确定存储的状态,因此您可以决定是否预先计算所有内容或部分(或全部)懒惰计算,并可能缓存,可能多线程等等...你明白了 :)


42
我不明白为什么“涂蜜糖”会被视为一件坏事。实际上,类和循环等东西也只是“涂蜜糖”而已。但“动手实践”的明显问题在于你基本上需要编写一个任意复杂的状态机(我可以想到一些现实应用场景,这并不容易)。 - Voo
4
@Voo:糖衣包装引入了复杂性,简单地说->有更多要学习的内容。 OP问及C++中的“yield”,我的看法是,与其将C#语法“移植”到C++中,最好反思它在做什么,并找出C++中惯用的方式。协程只不过是一个“输入迭代器”。 - Matthieu M.
24
根据我的经验,我不同意“引入了复杂性”的说法——生成器语义是简单易懂的(如果有一种语言不遵循“尽可能简单的语法”方法,那就是C++!)。此外,这并不是C#语法,而是计算机科学中一个众所周知的概念,实现在许多编程语言中(肯定不同于InputIterator!)。对于某些函数手动实现状态机通常非常困难。例如,请尝试使用InputerIterator实现this - 肯定更难理解。 - Voo
3
马修,for循环不就是对while循环的简化吗?switch语句不就是一连串if语句的级联吗?语法糖并不一定是坏事,如果没有它,我们仍需要直接将十六进制操作码输入到内存中。这只是一个画线的问题。你好像是把画线放在只有一个循环语句和一个分支语句的语言与包括yield的语言之间。其他语言包括yield。我用过yield,也知道它的作用,但我可以接受有或没有它。 - sbi
1
@Matthieu:所以对于lambda函数所做的转换是可以的,但是对于yield所需的转换则不行。这不就是我说的吗?这一切都归结于你在哪里划线。:) - sbi
显示剩余8条评论

14

自C++20标准库起,协程已经被引入,并使用co_yield代替yield

相关链接:什么是C++20中的协程?

第一个链接中有一些用法示例:(第二个可能是您要找的)

  • 使用co_await运算符暂停执行直到恢复

task<> tcp_echo_server() {
   char data[1024];
   while (true) {
      size_t n = co_await socket.async_read_some(buffer(data));
      co_await async_write(socket, buffer(data, n));
   }
}
使用关键字 co_yield 暂停执行并返回一个值。
generator<int> iota(int n = 0) {
   while (true)
      co_yield n++;
}
  • 使用关键字 co_return 来返回一个值并完成执行

  • lazy<int> f() {
       co_return 7;
    }
    

    这是为新潮的孩子们准备的。 - Alexis Paques

    13

    在C++14中,您可以通过以下方式模拟yield

    auto&& function = []() { 
        int i = 0; 
        return [=]() mutable { 
            int arr[] = { 1, 2, 4, 8, 16, 16777216}; 
            if (i < 6) 
                return arr[i++]; 
            return 0; 
        }; 
    }();
    

    这里有一个实时示例,可以在http://ideone.com/SQZ1qZ找到。


    2
    我没有购买,但是可以将实时示例轻松地放入您的答案中,而不显示来自ideone.com的广告。 - rwst
    14
    yield 的目的不是防止立即将对象序列(在这种情况下是 int[])放入内存吗? - Marc Dirven
    例子仍然有效,不需要预先计算数组,接下来看下一个例子。 `#include <iostream> int main() { auto&& function = [](int i0) { int i = i0; return = mutable { i *= 2; return i;}; };auto fn = function(5); for ( unsigned long i = 0; i != 10; ++i ) std::cout << "\t" << fn() << "\t|"; std::cout << "\n"; return 0;}` - karel

    4

    这里是 ASM “自己动手实现”的版本:http://www.flipcode.com/archives/Yield_in_C.shtml

    #include <stdio.h
    #include <conio.h
    #include <iostream.h
    
    
    //
    // marks a location in the program for resume
    // does not return control, exits function from inside macro
    //
    // yield( x, ret )
    //      x : the 'name' of the yield, cannot be ambiguous in the
    //          function namespace
    //    ret : the return value for when yield() exits the function;
    
    //          must match function return type (leave blank for no return type)
    
    #define yield(x,ret)                            \
        {                                           \
            /* store the resume location */         \
            __asm {                                 \
                mov _myStaticMkr,offset label_##x   \
            }                                       \
                                                    \
            /* return the supplied value */         \
            return ret;                             \
        }                                           \
        /* our offset in the function */            \
        label_##x:
    
    
    
    //
    // resumes function from the stored offset, or
    // continues without notice if there's not one
    // stored
    //
    // resume()
    //   <void
    
    #define resume()                        \
        /* our stored offset */             \
        static _myStaticMkr=0;              \
                                            \
        /* test for no offset */            \
        if( _myStaticMkr )                  \
        {                                   \
            /* resume from offset */        \
            __asm                           \
            {                               \
                jmp _myStaticMkr            \
            }                               \
        }
    
    
    // example demonstrating a function with an int return type
    // using the yield() and resume() macros
    //
    // myFunc()
    //   <void
    
    int myFunc()
    {
        resume();
    
        cout << "1\n";
    
        yield(1,1);
    
        cout << "2\n";
    
        yield(2,1);
    
        cout << "3\n";
    
        yield(3,1);
    
        cout << "4\n";
    
        return 0;
    }
    
    
    
    // main function
    //
    // main()
    //   <void
    
    void main( void )
    {
        cout << "Yield in C++\n";
        cout << "Chris Pergrossi\n\n";
    
        myFunc();
    
        do
    
        {
            cout << "main()\n";
            cout.flush();
        } while( myFunc() );
    
        cout.flush();
    
        getch();
    }
    
    
    /*
    
    // example demonstrating a function with no return type
    // using the yield() and resume() macros
    //
    // myFunc()
    //   <void
    
    void myFunc()
    {
        resume();
    
        cout << "1\n";
    
        yield(1);
    
        cout << "2\n";
    
        yield(2);
    
        cout << "3\n";
    
        yield(3);
    
        cout << "4\n";
    
        return;
    }
    
    
    
    // main function
    //
    // main()
    //   <void
    
    void main( void )
    {
        cout << "Yield in C++\n";
        cout << "Chris Pergrossi\n\n";
    
        myFunc();
    
        for( int k = 0; k < 4; k ++ )
        {
            cout << "main()\n";
            cout.flush();
    
            myFunc();
        }
    
        cout.flush();
    
        getch();
    }
    
    */  
    

    非常好,但这是跨平台的吗? - xilpex
    任何人都不应该使用这个,如果你使用了,请立即删除它。这涉及到 #UB,因为在恢复之前,您的堆栈数据可能会被任何操作覆盖。 - Alexis Paques
    @AlexisPaques,那不是未定义行为,而是必须手动处理堆栈,这在使用__asm时是我们所期望的。 - c z
    所有本地变量都已中断。一个适当的实现需要保存上下文以便能够恢复它。但这里并不是这种情况。 - Alexis Paques
    https://thisisub.godbolt.org/z/fh87sd1c4 - 随意自行检查 :) - Alexis Paques

    2
    如果您只需要类似于foreach的东西,那么在C ++中可用以下语法:
    #define GENERATOR(name) \
    struct name \
    { \
        template<typename F> \
        void operator()(F yield) \
    /**/
    #define _ };
    
    template<typename Gen>
    struct Adaptor
    {
        Gen f;
        template<typename C>
        void operator*(C cont)
        {
            f(cont);
        }
    };
    
    template<typename Gen>
    Adaptor<Gen> make_adaptor(Gen gen)
    {
        return {gen};
    }
    
    #define FOREACH(arg, gen) make_adaptor(gen) * [&](arg)
    

    #include <iostream>
    using namespace std;
    
    GENERATOR(integers)
    {
        yield(1);
        yield(2);
        yield(4);
        yield(8);
        yield(16777216);
    }_
    
    int main()
    {
        FOREACH(int i, integers())
        {
            cout << i << endl;
        };
    }
    

    演示实例

    如果您需要一点点协程 “能量”,那么可以尝试使用 无栈协程

    或者,如果您需要完整的能力 - 那么就选择有栈协程。有一个名为 Boost.Coroutine 的库,可为不同平台实现有栈协程。


    1

    尝试在C++中实现yield的方法协程


    1

    类似的东西也被提议用于 C++17,并且在 Visual C++ 2015 中已经有了实验性的实现。这里有一个很好的概述talk,来自提案的主要作者之一 Gor Nishanov。


    1
    如果你写了static unsigned int checkpoint = 0;,那么你需要让所有变量都是static类型,使用switch (checkpoint),将每个case: goto设置为某个标签,在每个return上方将checkpoint设置为唯一值,在下方定义标签,在函数末尾将checkpoint设置为零,并将所有静态变量设置为它们的默认值,最后return函数的结束值。如果你这样做,函数就会变得可枚举迭代。你在每个return行上方和下方添加的两行代码,使return命令的行为类似于yield returngoto允许你继续和恢复之前的工作状态,而像checkpoint这样的static整数变量则帮助你记住停止的位置、从哪里继续/恢复以及要去哪里。你可以使用switch case语句测试它的值。将所有其他变量设为static,是为了保存它们的值到下一次调用,这样在下一次调用时,它们的值就不会被重置!
    例如:
    #define PowerEnd INT_MIN
    int Power(int number, int exponent)
    {
        static unsigned int checkpoint = 0;
        static int result = 1, i = 0;
        switch (checkpoint)
        {
            case 1: goto _1;
        }
        for (i = 0; i < exponent; i++)
        {
            result *= number;
            checkpoint = 1;
            return result;
            _1:;
        }
        checkpoint = 0;
        result = 1;
        i = 0;
        return PowerEnd;
    }
    void main()
    {
        while (true)
        {
            int result = Power(2, 8);
            if (result == PowerEnd)
                break;
            cout << result << endl;
        }
        //to print only the first 4 results (if there are at least 4 results) then
        for (int i = 0; i < 4; i++)
        {
            int result = Power(2, 8);
            if (result == PowerEnd)
                break;
            cout << result << endl;
        }
    }
    

    上面的程序产生以下输出:

    2 4 8 16 32 64 128 256 2 4 8 16


    0
    #include <setjmp.h>
    
    class superclass
    {
    public:
        jmp_buf jbuf;
    public:
        virtual int enumerate(void) { return -1; }
    };
    
    class subclass: public superclass
    {
    public:
        int enumerate()
        {
            static int i;
            static bool b = false;
    
            if(b) 
                longjmp(jbuf, 1);
    
            for(b = true, i = 0; i < 5; (i)++)
            {
                printf("\ndoing stuff: i = %d\n", i);
    
                if(setjmp(jbuf) != 1) 
                    return i;    
            }
            return -1;
        }
    };
    

    使用代码...

    int iret; 
    subclass *sc;
    
    sc = new subclass();
    while((iret = sc->enumerate()) != -1)
    {
        printf("\nsc->enumerate() returned: %d\n", iret);
    }
    

    刚刚让这个工作起来了,现在看起来相当简单,尽管一开始有几次失败的尝试 :)


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接