关于向量:c 和优化中缺少返回的不稳定行为 | 珊瑚贝

Erratic behaviour with missing return in c++ and optimizations


假设您在 c 中编写了一个函数,但心不在焉地忘记输入单词 return。在那种情况下会发生什么?我希望编译器会抱怨,或者一旦程序到达那个点,至少会引发分段错误。然而,实际发生的情况要糟糕得多:程序吐出垃圾。不仅如此,实际输出还取决于优化的程度!这是一些演示此问题的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <vector>

using namespace std;

double max_1(double n1,
         double n2)
{
  if(n1>n2)
    n1;
  else
    n2;
}

int max_2(const int n1,
      const int n2)
{
  if(n1>n2)
    n1;
  else
    n2;
}

size_t max_length(const vector<int>& v1,
          const vector<int>& v2)
{
  if(v1.size()>v2.size())
    v1.size();
  else
    v2.size();
}

int main(void)
{
  cout << max_1(3,4) << endl;
  cout << max_1(4,3) << endl;

  cout << max_2(3,4) << endl;
  cout << max_2(4,3) << endl;

  cout << max_length(vector<int>(3,1),vector<int>(4,1)) << endl;
  cout << max_length(vector<int>(4,1),vector<int>(3,1)) << endl;

  return 0;
}

这是我在不同优化级别编译它时得到的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ rm ./a.out; g++ O0 ./test.cpp && ./a.out
nan
nan
134525024
134525024
4
4
$ rm ./a.out; g++ O1 ./test.cpp && ./a.out
0
0
0
0
0
0
$ rm ./a.out; g++ O2 ./test.cpp && ./a.out
0
0
0
0
0
0
$ rm ./a.out; g++ O3 ./test.cpp && ./a.out
0
0
0
0
0
0

现在假设您正在尝试调试函数 max_length。在生产模式下你会得到错误的答案,所以你在调试模式下重新编译,现在当你运行它时一切正常。

我知道有一些方法可以通过添加适当的警告标志 (-Wreturn-type) 来完全避免这种情况,但我仍然有两个问题

  • 为什么编译器甚至同意编译一个没有返回语句的函数?旧代码是否需要此功能?

  • 为什么输出取决于优化级别?

    • 1. 证明一个函数不是所有路径都有返回值并不容易。 2. 这是未定义的行为,因此您不能期望任何特定的输出。
    • 与为什么此 C 代码段编译相关(非 void 函数不返回值)。基本上,由于它是未定义的行为,因此您的结果是不可预测的,编译器因在优化期间利用 UB 而臭名昭著。在所有情况下都很难检测到这一点。
    • 你注意警告吗?将 -Werror 标志传递给 gcc,它不会编译。
    • John Regehr 的这篇文章:通过查找死代码来查找未定义的行为错误提供了一些编译器利用 UB 的有趣示例。
    • @Slava OP 知道如何让编译器对此发出警告,OP 询问为什么编译器甚至允许它,这是一个公平的问题,您不知道未定义的行为。
    • @juanchopanza 为什么很难证明函数在某些情况下不返回?难道它不能用可能的路径替换每个 if 并检查所有可能的布尔组合吗?
    • @juanchopanza 我知道在编译时可能很难,但是在运行时程序肯定会注意到它在遇到 return 语句之前已经到达函数的末尾。
    • 这不是那么明显。它必须进行仪表化,这将带来成本。
    • 请注意,正如尼尔指出的那样,当前接受的答案是不正确的,尽管他指出的问题并不是唯一的。


    这是丢弃值返回函数末尾的未定义行为,这在草案 C 标准部分 `6.6.31 的 return 语句中有所介绍:

    Flowing off the end of a function is equivalent to a return with no
    value; this results in undefined behavior in a value-returning
    function.

    编译器不需要发出诊断,我们可以从 1.4 实施合规性部分看到这一点:

    The set of diagnosable rules consists of all syntactic and semantic
    rules in this International Standard except for those rules containing
    an explicit notation that a€?no diagnostic is requireda€? or which are
    described as resulting in a€?undefined behavior.a€?

    尽管编译器通常会尝试捕获各种未定义的行为并产生警告,尽管通常您需要使用正确的标志集。对于 gcc 和 clang 我发现以下一组标志很有用:

    -Wall -Wextra -Wconversion -pedantic

    一般来说,我鼓励您使用 -Werror.

    将警告转化为错误

    编译器因在优化阶段利用未定义行为而臭名昭著,请参阅通过查找死代码查找未定义行为错误以获取一些很好的示例,包括在处理此代码时臭名昭著的 Linux 内核空指针检查删除:

    1
    2
    3
    struct foo *s =;
    int x = s>f;
    if (!s) return ERROR;

    gcc 推断由于 s 在 s->f; 中被引用,并且由于取消引用空指针是未定义的行为,因此 s 不能为空,因此优化了下一行的 if (!s) 检查(复制从我的回答这里)。

    由于未定义的行为是不可预测的,因此在更激进的设置下,编译器在许多情况下会进行更激进的优化,其中许多可能没有太大的直观意义,但是,嘿,这是未定义的行为,所以无论如何你都不应该有任何期望。

    请注意,尽管在很多情况下编译器可以确定函数在一般情况下没有正确返回,但这是暂停问题。在运行时自动执行此操作会产生违反不为您不使用的理念付费的成本。虽然 gcc 和 clang 都实现了清理程序来检查诸如未定义行为之类的事情,例如使用 -fsanitize=undefined 标志将在运行时检查未定义行为。


    您可能想在此处查看此答案

    唯一的原因是编译器允许你没有 return 语句,因为可能有许多不同的执行路径,确保每个都以 return 退出在编译时可能会很棘手,所以编译器会处理它给你。

    要记住的事情:

    如果 main 结束时没有返回,它将总是返回 0。


    如果另一个函数没有返回就结束它总是返回eax寄存器中的最后一个值,通常是最后一条语句

    优化会更改汇编级别的代码。这就是为什么你会出现奇怪的行为,编译器正在为你”修复”你的代码,当执行事情时会改变你的代码,给出不同的最后一个值,从而返回值。

    希望这有帮助!

    • 如果另一个函数没有返回就结束了,这是未定义的行为。
    • 是的,它是未定义的,因为你不能保证最后执行的语句是 eax 寄存器中的内容,但它是返回时使用的寄存器,因此是调用函数获取的内容。除非它当然是非整数类型,但它是 XMM 或 FPU 堆栈或任何类型。所以是的,它未定义,但通常它是最后一条语句的结果。
    • 不,它只是未定义。这与eax寄存器无关。可能它通常是您的机器和您的编译器上 eax 寄存器的最后一个值,但我可以让编译器在每次发生这种情况时返回 47 并且它将 100% 符合标准。


    来源:https://www.codenong.com/26237432/

    微信公众号
    手机浏览(小程序)
    0
    分享到:
    没有账号? 忘记密码?