我希望能够强制'双回',即有一个强制从调用函数返回的函数(是的,我知道并不总是真实的调用函数等。)显然我希望能够通过操作堆栈来做到这一点,并且我认为至少在一些非便携式机器语言方式中它是可能的。问题是这是否可以相对干净和便携地完成。
为了填写一段具体的代码,我想编写函数
void foo(int x) {
/* magic */
}
以便以下功能
int bar(int x) {
foo(x);
/* long computation here */
return 0;
}
返回,比方说,1
;并且不执行长计算。假设foo()
可以假设它只被带有bar签名的函数调用,即int(int)
(因此具体知道它的调用者返回类型是什么)。
注意:
bar()
) 。它不会知道所调用的函数是什么。 (再次在示例中,只能修改/* magic */
位)。答案 0 :(得分:8)
问题是这是否可以相对干净和便携地完成。
答案是它不能。
除了在不同系统上如何实现调用堆栈的所有非可移植细节之外,假设foo
被内联到bar
。然后(通常)它将没有自己的堆栈帧。您不能干净利落地或可移植地谈论逆向工程“双”或“n次”返回,因为实际的调用堆栈不一定看起来像您期望的基于C或C ++抽象的调用机。
您需要解决此问题的信息可能(无保证)可用于调试信息。如果调试器要向其用户显示“逻辑”调用堆栈,包括内联调用,则必须有足够的信息来定位“两级调高”调用者。然后,您需要模仿特定于平台的函数退出代码,以避免破坏任何内容。这需要恢复中间函数通常会恢复的任何内容,即使使用调试信息也可能不容易弄清楚,因为执行此操作的代码位于bar
某处。但我怀疑,因为调试器可以显示该调用函数的状态,所以至少在原理上调试信息可能包含足够的信息来恢复它。然后回到原始调用者的位置(可以通过显式跳转实现,或者通过操纵平台保存其返回地址并进行正常返回的任何位置)。所有这些都非常脏,非常不便携,因此我的“不”答案。
我假设您已经知道可以移植使用例外或setjmp
/ longjmp
。 bar
或bar
(或两者)的来电者需要与之合作,并同意foo
如何存储“返回值”。所以我认为这不是你想要的。但是如果修改bar
的调用者是可以接受的,你可以做这样的事情。它不漂亮,但它只是工作(在C ++ 11中,使用异常)。我会让你知道如何使用setjmp
/ longjmp
并使用固定的函数签名而不是模板在C中执行此操作:
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的行为有不明确的行为,所以你必须先决定是在处理C ++还是使用C语言。
答案 1 :(得分:4)
有两种便携式方法可以做到这一点,但都需要来电功能帮助。对于C,它是setjmp + longjmp。对于C ++,它是异常使用(try + catch + throw)。两者在实现上非常相似(实质上,一些早期的异常实现基于setjmp)。并且,如果没有调用者功能意识,肯定没有可移植的方法...
答案 2 :(得分:2)
唯一干净的方法是修改你的功能:
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()
来完成,但您也必须修改您的来电者,然后您也可以干净利落地完成。
答案 3 :(得分:1)
修改void函数foo()
以返回布尔值yes / no,然后将其包装在同名的宏中:
#define foo(x) do {if (!foo(x)) return 1;} while (0)
正如我相信,do .. while (0)
是标准的吞下分号技巧。
您可能还需要在声明foo()
的头文件中添加额外的括号,如:
extern bool (foo)(int);
这可以防止使用宏(如果已定义)。 foo()
实施同上。