当返回对象的函数在没有返回语句的情况下结束时会发生啥

Posted

技术标签:

【中文标题】当返回对象的函数在没有返回语句的情况下结束时会发生啥【英文标题】:What happens when a function that returns an object ends without a return statement当返回对象的函数在没有返回语句的情况下结束时会发生什么 【发布时间】:2016-12-31 07:04:20 【问题描述】:

在 C++ 中,当一个本应返回对象的函数在没有返回语句的情况下结束时会发生什么?返回什么?

例如

std::string func() 

【问题讨论】:

未定义的行为。 @πάνταῥεῖ,到目前为止,我从未对当前标准感到如此失望。 另一种“未定义行为”的情况,很容易被报告为编译器错误。有时它是一个警告:“并非所有控制路径都返回一个值”。 @BitTickler 有时,您可以通过业务逻辑证明永远无法到达控制路径,但编译器的静态分析器却不能。将其与语法上难以构造甚至不可能在相关函数中构造的返回类型(私有 ctor 等)结合起来,您就会遇到难以解决的错误,这在您的情况下并不是真正的错误。 @BitTickler 你误解了我的意思。我是说有时,你可以有一个没有return 的路径,它看起来可以访问,但实际上不是,因为上下文对编译器不可见(例如调用站点)。并且返回类型可能使得创建人工return 语句可能很困难。 【参考方案1】:

返回什么?

我们不知道。根据标准,行为是未定义的。

§6.6.3/2 The return statement [stmt.return]:

(强调我的)

从构造函数、析构函数或具有 cv void 返回类型的函数的末尾流出等效于没有操作数的 return。否则,从main (basic.start.main) 以外的函数末尾流出会导致未定义的行为

事实上大多数编译器都会给它一个警告,比如Clang:

警告:控制到达非空函数[-Wreturn-type]的末尾

【讨论】:

编译器有时会尝试使用默认构造函数构建对象吗? @MattMunson 如果愿意,可以。这是不可靠的。 @MattMunson 没有。在实践中,返回值要么包含在寄存器中,要么被 1 指向。在前一种情况下,寄存器可能包含也可能不包含正确的值。在后一种情况下,指针要么指向错误的位置,要么指向正确的(但未初始化的)内存。无论哪种方式,它都很糟糕。 @MattMunson 这是一个糟糕的想法。它会使代码更难调试。最好将垃圾返回,这样您最终会触发段错误或其他一些错误,并且至少您知道某处有问题并且您可以开始逐步调试。更好的是使用-Wall -Werror,这会使编译器在编译时检测到这个错误。【参考方案2】:

在 C++ 中,当一个应该返回对象的函数在没有 return 语句的情况下结束时会发生什么?

它会导致未定义的行为。没有人知道究竟会发生什么。

【讨论】:

有趣,我认为它可能会返回一个从默认构造函数构建的对象。不过,我想不是所有的班级都有一个。 @immibis 这确实意味着“无法预测会发生什么”。 回复:“没有人知道究竟会发生什么”:不,这被夸大了。这意味着 语言定义 不会告诉您会发生什么。您的编译器文档可能会告诉您。 @RoadieRich 根据标准合法,根据我所知道的任何 CPU 架构都不合法。即使是所谓的“停止并着火”指令也不是字面意思。 @immibis 看看这个LLVM blog about UB。关键是,是的,对于 some UB,编译器可能会提供定义。但总的来说,UB 的结果是任意的,因此是不可预测的。它可能会受到看似不相关的代码更改的影响,它可能会受到发生时的寄存器/缓存状态的影响,......也许它实际上是确定性的,但绝不是逻辑上可预测的。【参考方案3】:

我很好奇,所以在 Visual C++ 2015 上做了一些测试。

int f()

    if (false)
        return 42;

    // oops


int main()

    int i = f();

我必须添加 if 以获得警告而不是硬错误:

> cl /nologo /FAs /c a.cpp
a.cpp(6) : warning C4715: 'f': not all control paths return a value

生成的汇编代码非常简单,我已经删除了不相关的部分。这是f()的肉:

f:
    xor eax, eax
    je label
    mov eax, 42
label:
    ret

xor 行基本上是eax=0。因为if (false) 是一个常量条件,所以生成的代码甚至都懒得做比较,然后无条件地跳转到label,它只是从函数中返回。您可以看到“返回值”(42)实际上将存储在eax 中,但该行永远不会被执行。因此,eax == 0

main() 的作用如下:

    call f
    mov _i$[ebp], eax
    ret

它调用f() 并盲目地将eax 复制到堆栈上的某个位置(i 所在的位置)。因此,i == 0

让我们尝试一些更复杂的对象和构造函数:

struct S  int i=42; ;

S f()

    if (false)
        return ;

    // oops


int main()

    S s = f();

main()所做的基本上就是在栈上保留sizeof(S)字节,将第一个字节的地址放入eax,然后调用f()

    lea eax, _s$[ebp]
    push eax
    call f

同样,f() 不会做任何事情,因为它会无条件地跳转到函数的末尾:

f:
    xor eax, eax
    je label
    ; some stuff
    ; call S::S(), which would set i to 42
    ; but none of that will happen
label:
    ret

那么 main 中的 sizeof(S) 字节发生了什么?他们从未改变过。它们包含该特定位置已经存在于内存中的任何内容。它们包含垃圾。

这是在给定编译器的给定版本上使用未优化的构建。改变编译器,改变行为。启用优化器drastically change the behaviour。

别这样。

【讨论】:

以上是关于当返回对象的函数在没有返回语句的情况下结束时会发生啥的主要内容,如果未能解决你的问题,请参考以下文章

该函数要么返回 None 要么在没有返回语句的情况下结束,使用 docker 将 python 与 mysql 连接起来

为啥当objective-c 有冲突返回类型时会发生这种情况?

C++基础---无返回值函数(void函数)

10-1:return可以没有返回值吗?

在没有返回语句的情况下到达函数结尾

从函数返回的对象在不使用时是不是仍然创建?