Как осуществить возврат значения из вызывающей функции?

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

дать конкретный кусок кода для заполнения, Я хочу написать функцию

void foo(int x) {
    /* magic */
}

так что следующая функция

int bar(int x) {
    foo(x);
    /* long computation here */
    return 0;
}

возвращает, скажем, 1; и долгое вычисление не выполняется. Предположим, что foo() можно предположить, что он вызывается только функцией с сигнатурой бара, т. е. int(int) (и, таким образом, конкретно знает, что его вызывающий тип возврата).

Примечания:

  • пожалуйста, не читайте мне лекцию о том, как это плохая практика, я прошу из любопытства.
  • вызывающая функция (в Примере,bar()) не должен быть изменен. Он не будет знать, что делает вызываемая функция. (Опять же в Примере, только /* magic */ бит может быть изменен).
  • если это поможет, вы можете предположить, что нет инлайнинга (возможно, нереалистичное предположение).

4 ответов


вопрос в том, можно ли это сделать относительно чисто и переносимо.

ответ в том, что это невозможно.

помимо всех непереносимых деталей того, как стек вызовов реализуется в разных системах, предположим foo получает встроен в bar. Тогда (как правило) у него не будет собственного кадра стека. Вы не можете чисто или переносно говорить о обратном проектировании возврата" double "или" n-times", потому что фактический стек вызовов не обязательно выглядит так, как вы ожидали бы, основываясь на вызовах, сделанных абстрактной машиной C или c++.

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

я предполагаю, что вы уже знаете, что вы можете использовать исключения илиsetjmp / longjmp. Либо bar или вызывающей bar (или оба) должны были бы сотрудничать с этим и согласиться с foo как хранится "возвращаемое значение". Так что, полагаю, это не то, чего ты хочешь. Но если изменить вызывающийbar допустимо, вы могли бы сделать что-то подобное. Это не очень красиво, но это просто работает (в C++11, используя исключения). Я оставлю это вам, чтобы выяснить, как это сделать в C с помощью setjmp / longjmp и с фиксированной сигнатурой функции вместо шаблона:

template <typename T, typename FUNC, typename ...ARGS>
T callstub(FUNC f, ARGS ...args) {
    try {
        return f(args...);
    }
    catch (EarlyReturnException<T> &e) {
        return e.value;
    }
}

void foo(int x) {
    // to return early
    throw EarlyReturnException<int>(1);
    // to return normally through `bar`
    return;
}

// bar is unchanged
int bar(int x) {
    foo(x);
    /* long computation here */
    return 0;
}

// caller of `bar` does this
int a = callstub<int>(bar, 0);

наконец, не "плохая практика лекция", а практическое предупреждение-использование любой трюк, чтобы вернуться раньше, как правило, не идет хорошо с кодом, написанным на C или написанным на C++, который не ожидает исключение оставить foo. Причина в том, что bar возможно, выделили некоторый ресурс или поместили некоторую структуру в состояние, которое нарушает ее инварианты перед вызовом foo, с намерением освободить этот ресурс или восстановить инвариант в коде после вызова. Итак, для общих функций bar, Если вы пропустите код bar тогда вы можете вызвать утечку памяти или недопустимое состояние данных. Единственный способ избежать этого вообще, независимо от того, что находится в bar, чтобы позволить остальные bar запустить. Конечно, если bar написано на C++ с ожиданием, что foo может бросить, тогда он будет использовать RAII для кода очистки, и он будет работать, когда вы бросаете. longjmping над adestructor имеет неопределенное поведение, поэтому вы должны решить, прежде чем начать, имеете ли вы дело с C++ или с C.


есть два портативных способа сделать это, но оба требуют помощи функции вызывающего абонента. Для C это setjmp + longjmp. Для C++ это использование исключений (try + catch + throw). Оба довольно схожи по реализации (по сути, некоторые ранние исключения были основаны на setjmp). И, безусловно, нет портативного способа сделать это без осознания функции вызывающего абонента...


единственный чистый способ-изменить свои функции:

bool foo(int x) {
    if (skip) return true;
    return false;
}

int bar(int x) {
    if (foo(x)) return 1;
    /* long computation here */
    return 0;
}

и setjmp() / longjmp(), но вам также придется изменить вызывающего абонента, а затем вы также можете сделать это чисто.


измените функцию void foo() чтобы вернуть логическое да / нет, а затем обернуть его в макрос с тем же именем:

    #define foo(x) do {if (!foo(x)) return 1;} while (0)

на do .. while (0) это, как я уверен, вы знаете, стандартный трюк с точкой с запятой.

ваш также может понадобиться в заголовочном файле, где вы объявляете foo(), чтобы добавить дополнительные скобки, например:

    extern bool (foo)(int);

это предотвращает использование макроса (если он уже определен). То же самое для foo() реализация.