Я хотел бы иметь возможность принудительного «двойного возврата», то есть иметь функцию, которая вызывает возврат из его призвание функция (да, я знаю, что не всегда существует реальная функция вызова и т. д.) Очевидно, я ожидаю, что смогу сделать это, манипулируя стеком, и я предполагаю, что это возможно, по крайней мере, некоторым непереносимым способом машинного языка. Вопрос в том, можно ли сделать это относительно чисто и переносимо.
Чтобы дать конкретный кусок кода для заполнения, я хочу написать функцию
void foo(int x) {
/* magic */
}
так что следующая функция
int bar(int x) {
foo(x);
/* long computation here */
return 0;
}
возвращает, скажем, 1
; и длинные вычисления не выполняются. Предположим, что foo()
можно предположить, что он вызывается только функцией с сигнатурой bar, т.е. int(int)
(и, таким образом, конкретно знает, каков его тип возврата вызывающей стороны).
Заметки:
bar()
) не должен быть изменены. Он не будет знать, для чего предназначена вызываемая функция. (Опять же в примере только /* magic */
бит можно изменить).Вопрос в том, можно ли сделать это относительно чисто и переносимо.
Ответ в том, что это невозможно.
Помимо всех непереносимых деталей того, как стек вызовов реализован в разных системах, предположим, foo
встраивается в bar
, Тогда (как правило) у него не будет своего собственного стекового фрейма. Вы не можете чисто или переносимо говорить об обратном инжиниринге «двойного» или «n-временного» возврата, потому что реальный стек вызовов не обязательно выглядит так, как вы ожидаете, основываясь на вызовах, сделанных C или C ++ abstract машина.
Информация, которую вам нужно взломать это наверное (без гарантий) доступно с отладочной информацией. Если отладчик собирается представить «логический» стек вызовов своему пользователю, включая встроенные вызовы, то должно быть достаточно информации, чтобы найти вызывающего абонента «на два уровня вверх». Затем вам нужно имитировать код завершения функции для конкретной платформы, чтобы ничего не сломать. Это требует восстановления всего, что обычно восстанавливает промежуточная функция, что может быть непросто понять даже с помощью отладочной информации, потому что код для этого находится в 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 для очистки кода и будет работать, когда вы бросаете. longjmp
В то же время, использование overstructor имеет неопределенное поведение, поэтому перед началом работы вы должны решить, имеете ли вы дело с 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()
, но вы также должны изменить своего вызывающего абонента, и тогда вы можете сделать это аккуратно.
Изменить вашу пустую функцию foo()
вернуть логическое значение да / нет, а затем обернуть его в макрос с тем же именем:
#define foo(x) do {if (!foo(x)) return 1;} while (0)
do .. while (0)
это, как я уверен, вы знаете, стандартная уловка ласточки с запятой.
Вам также может понадобиться в заголовочном файле, где вы объявляете foo()
, чтобы добавить дополнительные скобки, как в:
extern bool (foo)(int);
Это предотвращает использование макроса (если он уже определен). То же самое для foo()
реализация.