c++和优化中缺少返回的不稳定行为

Erratic behaviour with missing return in c++ and optimizations

本文关键字:不稳定 返回 优化 c++      更新时间:2023-10-16

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

#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;
}

当我在不同的优化级别编译它时,我会得到以下结果:

$ 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++标准草案`6.6.31返回语句中有介绍,它说:

从函数末尾流出相当于不返回价值这会导致返回值时出现未定义的行为作用

编译器不需要发布诊断,我们可以从1.4实现合规性一节中看到这一点,该节说:

可诊断规则集由所有句法和语义组成本国际标准中的规则,但包含"无需诊断"或被描述为导致"未定义的行为"

尽管编译器通常会尝试捕获大量未定义的行为并产生警告,但通常需要使用正确的标志集。对于gccclang,我发现以下标志集很有用:

-Wall-Wextra-Wconversion-迂腐的

一般来说,我鼓励您使用-Werror将警告转化为错误。

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

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;

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

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

请注意,尽管在许多情况下,编译器可以确定函数在一般情况下没有正确返回,但这是停止问题。在运行时自动执行此操作会带来成本,这违反了"不使用即不付费"的理念。尽管gccclang都实现了清理程序来检查未定义的行为,例如,使用-fsanitize=undefined标志将在运行时检查未定义行为。

您可能想在这里查看这个答案

只是编译器允许你没有返回语句,因为可能有很多不同的执行路径,确保每个执行路径都带着返回退出在编译时可能很棘手,所以编译器会帮你处理它。

需要记住的事项:

如果main结束时没有返回,它将始终返回0。

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

优化会更改程序集级别的代码。这就是为什么你会得到这种奇怪的行为,编译器正在为你"修复"你的代码——当执行时会给出不同的最后值,从而返回值。

希望这能有所帮助!