如何从“调用”函数中返回一个值?

7
我希望能够强制执行“双重返回”,即具备一个能够从其调用函数中强制性返回的函数(是的,我知道并不总是存在真正意义上的调用函数等)。很明显,我希望通过操作栈来完成这个过程,并且也认为至少在某些不可移植的机器语言方式下是可行的。问题在于,这是否可以以相对干净和可移植的方式实现。
为了提供一个具体的代码片段,我想写出这个函数。
void foo(int x) {
    /* magic */
}

为了使以下函数

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

假设foo()可以假定它仅被具有bar签名的函数调用,例如一个int(int)(因此特别知道其调用者的返回类型),则返回值为1;长计算未执行。 注意:
  • 请不要对我进行讲解,说明这是不好的做法,我只是出于好奇在问。
  • 调用函数(在示例中为bar()不得被修改。 它将不知道所调用的函数正在做什么。(同样在示例中,只能修改/* magic */部分)。
  • 如果有帮助,请假设没有进行内联(这可能是不切实际的假设)。

这不仅是一种糟糕的实践方式;而且如果不修改“bar”或使用一些愚蠢的异常技巧,这是不可能的。这种情况完全是结构化编程运动所反对的(我应该补充说,这种反对是合理和成功的)。 - cHao
1
内部函数如何知道调用者的返回类型?! - Kerrek SB
“相对干净且可移植”是高度主观的。投票关闭因为基于意见。 - n. m.
@KerrekSB:这是基于调用者类型的假设得出的结论。现在我已经更明确地表达了它。 - einpoklum
4个回答

8
问题是是否能相对干净和可移植地完成这个操作。
答案是否定的。除了在不同系统上实现调用栈的非可移植细节之外,假设foo被内联到bar中。那么(通常)它将不会有自己的堆栈帧。你无法干净或可移植地谈论关于“双倍”或“n次”返回的逆向工程,因为实际的调用栈并不一定像C或C++抽象机所做的调用那样。
要破解它所需的信息可能(没有保证)可以使用调试信息。如果调试器将向其用户呈现“逻辑”调用堆栈,包括内联调用,则必须有足够的信息可用于定位“两个级别以上”的调用方。然后,您需要模仿特定于平台的函数退出代码以避免破坏任何内容。这要求恢复中间函数通常会恢复的任何内容,即使有调试信息也可能很难弄清楚,因为执行此操作的代码在某个地方是在bar中。但我怀疑,由于调试器可以显示该调用函数的状态,因此至少原则上调试信息可能包含足够的信息来恢复它。然后返回到原始调用者的位置(可以通过显式跳转或通过操作平台保存其返回地址的位置并执行普通返回来实现)。所有这些都非常凌乱和非常不可移植,因此我的答案是“不”。
我假设您已经知道可以使用异常或setjmp/longjmp进行可移植的编码。要协作实现这一点,barbar的调用者(或两者)都需要同意与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。原因是,在调用foo之前,bar可能已经分配了一些资源,或者将某些结构置于违反其不变量的状态下,意图在调用后释放该资源或在代码中恢复不变量。因此,对于一般函数bar,如果您跳过了bar中的代码,则可能会导致内存泄漏或无效的数据状态。不论bar中有什么,避免这种情况的唯一方法就是允许bar的其余部分运行。当然,如果bar是使用C++编写的,并且期望foo可能抛出异常,则它将使用RAII进行清理代码,当您抛出时它将运行。然而,直接跳过析构函数的行为未定义,因此您必须在开始之前决定您处理的是C ++还是C。


4

有两种可移植的方法可以实现此功能,但两种方法都需要调用函数的帮助。对于C语言,使用setjmp + longjmp。对于C++语言,使用异常处理(try + catch + throw)。两者在实现上非常相似(实质上,早期的异常处理实现是基于setjmp的)。而且,没有不需要调用函数知晓的可移植方式来实现这一点...


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()来实现,但您还需要修改调用者,然后您也可以干净地完成它。


1
@einpoklum:那显然,你很惨了。 :) 子程序无法以那种精度可靠和可移植地从其调用者那里夺取控制权。(那些看起来能够做到的少数情况,要么最终回到调用者,要么杀死进程,因此混乱是无关紧要的。) 你必须知道调用者的工作方式,如果bar可以是任意接受并返回int的函数,那么你不知道堆栈的布局如何。 - cHao

1

将您的无返回值函数foo()修改为返回布尔值(是/否),然后将其包装在同名宏中:

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

"

do .. while (0)是标准的吞掉分号技巧。

在声明foo()的头文件中,您可能还需要添加额外的括号,例如:

"
    extern bool (foo)(int);

这将防止已定义的宏被使用。对于foo()的实现也是如此。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接