C++ force stack unwinding inside function

Refresh

February 2019

Views

1.6k time

1

I'm in the process of learning C++ and currently I'm fiddling with the following code:

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);
}

The main idea is that I know that at every site result.Continue is called, the function Foo will return too. Therefore the stack can be unwound before calling the continuation.

As the user code will use this in a recursive way, I'm worried that this code may lead into stackoverflows. As far as my understanding goes, the parameters _x and result are kept on the stack when result.Continue is executed, because the stack is unwound only when Foo returns.

Edit: The Continue function may (and probably will) call the Foo method: resulting in recursion. Simply tail-call optimizing Continue and not Foo can lead to a stackoverflow.

What can I do to force unwinding of the stack, before the return of Foo, keeping result in a temporary (register?) variable and then execute that continuation?

4 answers

0

Unless I misunderstood, why not something like this (a single function causing a stackoverflow is a design flaw imo, but if there are lots of locals in your original Foo() then calling DoFoo() may alleviate the problem):

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

// ...

enum { use_x, use_y };

int DoFoo(Bar& _x)
{
    // Do stuff with _x

    if(/* some condition */) {
        return use_x;
    }

    // Do more stuff with _x

    if(/* some other condition */) {
        return use_x;
    }

    return use_y;
}

void Foo(Bar& _x, Callback& result)
{
    int result = DoFoo(_x);
    if (result == use_x)
    {
       result.Continue(_x);
       return;
    }

    Bar y; // allocate something on the stack
    result.Continue(y);
}
0

You can't explicitly force stack unwinding as you call it (destruction of _x and result in the code sample) before the function ends. If your recursion (you didn't show it) is amenable to tail call optimisation then good compilers will be able to handle the recursion without creating a new stack frame.

4

Вы можете использовать конструкцию я обнаружил, что решает эту проблему. Конструкция предполагает программу управляемых событий (но вы можете создать поддельный цикл обработки событий в противном случае).

Для наглядности, давайте забудем о вашей проблеме , и вместо того, чтобы сосредоточиться на проблеме интерфейса между двумя объектами: а отправителем объектом отправкой пакетов данных в приемнике объект. Отправитель всегда должен ждать приемника для завершения обработки любого пакета данных перед отправкой другого. Интерфейс определяется двумя вызовами:

  • Отправить () - вызываются отправителем, чтобы начать отправку пакета данных, реализованный приемник
  • Готово () - вызывается приемником для информирования отправителя о том, что операция передачи завершена, и можно отправить больше пакетов

Ни один из этих вызовов не возвращает ничего. Приемник всегда сообщает о завершении операции с помощью вызова Done (). Как вы можете видеть, этот интерфейс концептуально похож на то, что вы представили, и страдает от тех же проблем рекурсии между Send () и Done (), что может привести к переполнению стека.

Мое решение было введение очереди заданий в цикл обработки событий. Очереди заданий является очередью LIFO (стек) событий , ожидающих отправки. Цикл событий обрабатывает задание на вершине очереди как максимальный приоритет событие в. Другими словами, когда цикл событий должен решить , какое событие для отправки, он будет всегда отправлять верхнюю работу в очереди заданий , если очередь не пуста, а не любое другое событие.

Интерфейс , описанный выше, затем изменен , чтобы сделать как Send () и Done () вызовы в очереди . Это означает , что , когда вызовы отправителя Send (), все , что происходит в том , что работа выталкивается в очередь заданий, и эта работа, когда отправляется в цикл обработки событий, будет вызывать реальную реализацию получателя из Send (). Готово () работает точно так же - называется приемником, он просто нажимает на работу , которая, когда отправляется, вызывает реализацию отправителя из Done ().

Посмотрите, как дизайн очереди обеспечивает три основных преимущества.

  1. Это позволяет избежать переполнения стека, потому что нет никакой явной рекурсии между Send () и Done (). Но отправитель может до сих пор называют Send () снова прямо из его Done () обратного вызова, и приемник может вызова Done () прямо из его Send () обратного вызова.

  2. Она размывает различие между (I / O) операций, завершивших немедленно и те, которые имеют какое-то время, то есть приемник должен ждать какого-либо события на системном уровне. Например, при использовании не блокирующих сокетов, реализация Send () в приемнике вызывает Send () системный вызов, который либо удается отправить что-то, или возвращает EAGAIN / EWOULDBLOCK, в этом случае приемник запрашивает цикл событий, чтобы сообщить это когда сокет для записи. Он повторит Send () системный вызов, когда он сообщил цикл событий, что сокет записи, который, вероятно, преуспевает, в этом случае он информирует отправителя о том, что операция завершена с помощью вызова Done () из этого обработчика событий. Какой бы ни случилось, это то же самое с точки зрения отправителя - его Done функция () вызывается, когда операция отправки завершена, сразу же или через некоторое время.

  3. Это делает обработку ортогональной к фактическому ошибка ввода / вывода. Обработка ошибок может быть реализована при наличии приемника вызвать ошибки () обратный вызов , который обрабатывает ошибку , так или иначе. Посмотрите , как отправитель и получатель могут быть независимыми многоразовые модули , которые ничего об ошибках не знаю . В случае ошибки (например , отправить () системный вызов завершается с кодом реальной ошибки, а не EAGAIN / EWOULDBLOCK), отправитель и получатель могут быть просто уничтожены из Error () обратного вызова, которая, вероятно , часть одного и того же кода, созданного отправителя и приемник.

Вместе эти функции позволяют элегантное программирование на основе потоков в рамках программ , управляемые событий. Я выполнил проектирование очереди и программирование на основе потоков в моем BadVPN программного проекта, с большим успехом.

Наконец, некоторые разъяснения о том, почему очередь работа должна быть LIFO. Политика планирования ЛИФО обеспечивает крупнозернистый контроль над порядком диспетчерских рабочих мест. Например, предположим , что вы вызываете какой - либо метод какого - либо объекта, и вы хотите сделать что - то после того, как этот метод выполняется, и после всех работ она подтолкнула были посланы, рекурсивно. Все , что вам нужно сделать , это нажать на работу своего собственного права перед вызовом этого метода, и делать свою работу из обработчика события этой работы.

Существует также хорошая недвижимость, что вы всегда можете отменить эту отложенную работу работы извлечения из. Например, если что-то эта функция сделала (в том числе рабочих мест, она подтолкнула) привела к ошибке и, как следствие разрушения нашего собственного объекта, наш деструктор может из очереди на работе, которую мы выдвинули, избегая аварии, что случится, если работа выполнена, и доступа к данным не, что больше не существует.

0

Я нашел еще один способ, но это относится только к Windows, и Visual C ++:

void* growstk(size_t sz, void (*ct)(void*))
{
    void* p;
    __asm
    {
        sub esp, [sz]
        mov p, esp
    }
    ct(p);
    __asm
    {
        add esp, [sz]
    }
}

Продолжение void (*ct)(void*)будет иметь доступ к void* p;памяти стека выделяется. Всякий раз , когда продолжение возвращает память освобождается путем восстановления указателя стеки espна обычный уровень.