c++ - C++强制堆栈内部函数展开

标签 c++ stack stack-overflow continuations

我正在学习C++,目前正在摆弄以下代码:

class Bar;
struct Callback {
    virtual void Continue(Bar&) = 0;
};

// ...

void Foo(Bar& _x, Callback& result)
{
    // Do stuff with _x

    if(/* some condition */) {
        // TODO: Force unwind of stack
        result.Continue(_x);
        return;
    }

    // Do more stuff with _x

    if(/* some other condition */) {
        // TODO: Force unwind of stack
        result.Continue(_x);
        return;
    }

    // TODO: Force unwind of stack
    Bar y; // allocate something on the stack
    result.Continue(y);
}

主要思想是,我知道在每个站点result.Continue都被调用时,函数Foo也将返回。因此,可以在调用延续之前将堆栈展开。

由于用户代码将以递归方式使用它,因此我担心此代码可能会导致堆栈溢出。据我所知,执行_x时,将参数resultresult.Continue保留在堆栈上,因为仅当Foo返回时才取消堆栈的堆栈。

编辑:Continue函数可能(可能会)调用Foo方法:导致递归。简单地对Continue而不是Foo进行尾部调用优化可能会导致堆栈溢出。

我该怎么做才能在Foo返回之前强制展开堆栈,将result保留在一个临时(register?)变量中,然后执行该继续操作?

最佳答案

您可以使用我发现的可解决此问题的设计。该设计假定使用事件驱动程序(但是您可以创建假事件循环)。

为了清楚起见,让我们忘记您的特定问题,而将注意力放在两个对象之间的接口(interface)问题上:发送者对象将数据包发送到接收者对象。发送者总是必须等待接收者完成对任何数据包的处理,然后再发送另一个。该接口(interface)由两个调用定义:

  • Send()-由发送方调用以开始发送数据包,由接收方
  • 实现
  • Done()-接收方调用,以通知发送方发送操作已完成,并且可以发送更多数据包

  • 这些调用均未返回任何内容。接收者总是通过调用Done()来报告操作完成。如您所见,此接口(interface)在概念上与您介绍的接口(interface)相似,并且遭受Send()和Done()之间递归的相同问题,可能导致堆栈溢出。

    我的解决方案是将作业队列引入事件循环。作业队列是等待分派(dispatch)的事件的 LIFO队列(堆栈)。事件循环将队列顶部的作业视为最大优先级事件。换句话说,当事件循环必须决定要分派(dispatch)哪个事件时,如果队列不为空,并且没有任何其他事件,它将始终分派(dispatch)作业队列中的最高作业。

    然后修改上述接口(interface),以使Send()和Done()调用排队。这意味着,当发送方调用Send()时,所有发生的事情是将作业推送到作业队列,并且该作业在由事件循环调度时,将调用接收方的Send()的实际实现。 Done()以相同的方式工作-由接收方调用,它只是推送一个作业,该作业在分派(dispatch)时调用发送方的Done()实现。

    了解队列设计如何提供三个主要好处。
  • 避免了堆栈溢出,因为Send()和Done()之间没有显式的递归。但是发送者仍然可以直接从其Done()回调中再次调用Send(),接收者可以直接从其Send()回调中调用Done()。
  • 它模糊了立即完成的(I / O)操作与需要一些时间的操作之间的差异,即接收器必须等待某些系统级事件。例如,当使用非阻塞套接字时,接收器中Send()的实现将调用send()系统调用,该调用可以管理发送内容,或者返回EAGAIN / EWOULDBLOCK,在这种情况下,接收器会要求事件循环通知套接字可写时使用。当事件循环通知套接字可写时,它会重试send()系统调用,这很可能会成功,在这种情况下,它将通过从此事件处理程序中调用Done()来通知发送方操作已完成。无论发生哪种情况,从发送者的 Angular 来看都是相同的-在发送操作完成后,立即或一段时间后调用其Done()函数。
  • 使错误处理与实际I / O正交。可以通过让接收方调用以某种方式处理错误的Error()回调来实现错误处理。了解发送方和接收方如何成为独立可重用模块,即不了解有关错误的任何信息。如果发生错误(例如send()syscall失败并显示实际错误代码,而不是EAGAIN / EWOULDBLOCK),则可以简单地从Error()回调中破坏发送方和接收方,这很可能是创建发送方的同一代码的一部分和接收器。

  • 这些功能一起在事件驱动程序中启用了优雅的flow-based programming。我已经在BadVPN软件项目中实现了队列设计和基于流的编程,并取得了巨大的成功。

    最后,澄清为什么作业队列应该是LIFO。 LIFO调度策略提供了对作业分配顺序的粗粒度控制。例如,假设您正在调用某个对象的某个方法,并且想要在该方法执行后,并且递归地调度了它推送的所有作业之后执行某些操作。您要做的就是在调用此方法之前先完成自己的工作,然后从该事件的事件处理程序中进行工作。

    还有一个不错的属性,您始终可以通过使作业出队来取消此推迟的工作。例如,如果此函数执行的某些操作(包括它推送的作业)导致错误并因此破坏了我们自己的对象,则析构函数可以使我们推送的作业出队,从而避免了在执行作业和访问数据时发生崩溃不再存在。

    关于c++ - C++强制堆栈内部函数展开,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10064229/

    相关文章:

    c++ - 在 C++ 构造函数中使用 new 运算符是否正确?

    c++ - 'linesize alignment' 是什么意思?

    c++ - 如何在使用 QQmlApplicationEngine 时从 C++ 访问我的 Window 对象属性?

    Swift - 帮助破译调用堆栈

    c - 哪个是初始堆栈指针的正确值?

    java - 为什么在针对 XSS 攻击模式进行验证时,非常大的字符串会抛出 java.lang.StackOverflow 异常

    c++ - MAD(乘、加、除)散列函数如何工作?

    java - 为什么我应该使用 Deque 而不是 Stack,使用 LinkedList 而不是 Queue?

    java - 如果 Controller 在jMeter线程组中

    php - 导致 PHP 崩溃