问题是这是否可以相对干净和便携地完成。
答案是它不能。
除了调用堆栈如何在不同系统上实现的所有不可移植的细节之外,假设foo
被内联到bar
. 然后(通常)它不会有自己的堆栈帧。您不能干净或可移植地谈论对“双倍”或“n 次”返回进行逆向工程,因为实际调用堆栈不一定看起来像您根据 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 进行清理代码,并且会在您抛出时运行。longjmp
但是,对 adestructor 的 ing 具有未定义的行为,因此您必须在开始之前决定是使用 C++ 还是使用 C。