这种在g++中的优化是否可取?

4
我遇到了g++(版本10.2.1)的一个(对我来说)意外行为。这里是一个最简示例:
#include <iostream>

int f(int x) {
  if(x==0) {
    std::cout << "I am here" << std::endl;
    return 5;
  }
}

int main(int argc, char** argv) {

  int x = f(atoi(argv[1]));

  std::cout << x << std::endl;
  return 0;
}

我知道代码并没有太多意义,因为如果 x 不等于 0,f 函数并不会返回任何值。当我使用 g++ -Wall -O3 -DNDBUG -o bla bla.cpp 编译时,编译器也会自然地对此发出警告。如果我无论如何运行程序,将会发生以下情况:
$ ./bla 0
I am here
5
$ ./bla 1
I am here
5
$ ./bla 2
I am here
5
$ ./bla 3
I am here
5

我明白了 - 编译器看到唯一可能的返回值在if语句内部,所以它移除了if语句。然而,在我的实际(非最小化)代码中,我花了几个小时才明白这一点:我的函数f还有一个else分支,但为了测试新功能,我将其注释掉了,结果出现了奇怪的行为。
我的问题是,这是否是一种合理的优化,还是应该被视为编译器的错误?
编辑:谢谢大家快速回答。是的,正如我所说,我也收到了警告。我同意应该认真对待警告。但是,为了提供一个更接近我实际情况的例子:我甚至在调用f的函数中忽略了返回值 - 函数的主要“输出”是通过引用传递的,实际输出只是额外的值。所以我的(错误)想法是,如果代码返回一个无意义的值,我不需要它(再次强调,仅用于测试目的),这并不重要。我仍然觉得奇怪的是,如果我将下面的示例中的"int f.."替换为"void f",并将"return 5"替换为"return",它的行为就会改变。
#include <iostream>

int f(int x,int& z) {
  if(x==0) {
    std::cout << "I am here" << std::endl;
    z=7;
    return 5;
  }
}

int main(int argc, char** argv) {

  int z=1;

  f(atoi(argv[1]),z);

  std::cout << z << std::endl;
  return 0;
}

输出

$ ./bla 0
I am here
7
$ ./bla 1
I am here
7
$ ./bla 2
I am here
7
$ ./bla 3
I am here
7

最终编辑:感谢讨论。总结一下(如果以后有人看到这个):我原本认为,如果函数末尾缺少一个返回值,那么只是函数的返回值未定义(在扩展示例中可能还可以接受)。但实际上,整个函数的行为都是未定义的,这远不止于此。

5
不仅没有太多意义,而且还存在未定义行为。你的函数 f 承诺 返回一个 int 值。但是在函数中有一些路径并没有这样做,导致了上述的未定义行为。如果编译器还没有对此发出警告,请启用更多的警告。并将警告视为错误。这绝对不是编译器的错误,如果你的代码存在未定义行为,那么编译器基本上可以随意处理。 - Some programmer dude
4
你的代码存在_未定义行为_。你不应该期望得到任何合理的结果。 - πάντα ῥεῖ
4
对于未定义行为,你能做的就是将其移除。试图推理未定义行为是毫无意义的,正如行为名称所暗示的那样。 - Richard Critten
3
我认为与其说“编译器可以假设x==0为真”,更有帮助的是认为“当x==0为假时,编译器可以自由决定要做任何该死的事情”。如果x==0为假,你未能从一个有返回值的函数中返回一个值,因此行为是未定义的。编译器可以选择任何行为。它选择了“如果x==0为真,我将产生相同的行为”。 - Nathan Pierson
3
我认为与其说“编译器可以假设x==0为真”,更有帮助的是认为“当x==0为假时,编译器可以自由地做任何它想做的事情”。如果x==0为假,你未能从一个有返回值的函数中返回一个值,因此行为是未定义的。编译器可以选择任何行为。它选择了“如果x==0为真,我将产生相同的行为”。 - undefined
显示剩余31条评论
1个回答

13
我的问题是,这是否是一种合理的优化,还是应该被视为编译器中的一个错误?
这是可以接受的。编译器可以假设您的程序是以不会发生未定义行为的方式编写的,并且可以基于这个假设进行优化。
如果您的程序没有达到这个要求,结果可能会非常令人困惑,但这是在创建具有定义行为的程序时快速获得良好优化的代价。

1
无论在程序的哪个地方使用UB,整个程序从一开始就会有UB。只有当从一开始就无法避免UB时,整个程序才会是UB(全局)。如果执行进入一个分支,其中UB变得不可避免,那么程序才会是UB(全局)。如果将对f的调用更改为始终传递0,则所示的代码是正确的。编译器只能假设永远不会进入这样的分支。整个程序是正确的,但隐含的前提是argv[1]必须为0。 - François Andrieux
1
无论在程序的哪个地方使用UB,整个程序从一开始就会有UB。只有当从一开始就无法避免UB时,整个程序才会出现UB。如果执行进入一个分支,其中无法避免UB,那么它才是UB(程序范围)。如果将对f的调用更改为始终传递0,那么所示的代码就是正确的。编译器只能假设永远不会进入这样的分支。整个程序是正确的,但有一个隐含的前提条件,即argv[1]必须为0。 - undefined
1
@TedLyngmo [intro.compliance.genera #(2.2)] "...如果一个程序包含了违反规则但不需要诊断的情况,本文档对于实现程序没有任何要求..." - Richard Critten
1
@TedLyngmo [intro.compliance.genera #(2.2)] "...如果一个程序包含了一个规则的违反,而不需要进行诊断,那么本文档对于实现程序没有任何要求..." - undefined
1
否则,从既不是主函数([basic.start.main])也不是协程([dcl.fct.def.coroutine])的函数流出结果将导致未定义行为。f只有在条件不满足时才会产生未定义行为。如果条件得到满足,程序的行为必须正确且不会产生未定义行为。函数可能流出结束并不会导致未定义行为,只有在实际发生流出的情况下才会产生未定义行为。因为运行./bla 0不会导致函数流出结束,所以在这种情况下程序是定义良好的。 - François Andrieux
显示剩余8条评论

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