草庐IT

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

codeneng 2023-03-28 原文

Erratic behaviour with missing return in c++ and optimizations

假设您在 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 代码段编译相关(非 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€?

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

    -Wall -Wextra -Wconversion -pedantic

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

    将警告转化为错误

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

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

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

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

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


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

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

    要记住的事情:

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

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

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

    希望这有帮助!

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

    有关关于向量:c 和优化中缺少返回的不稳定行为的更多相关文章

    1. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

      为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

    2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

      我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

    3. ruby - 无法在 60 秒内获得稳定的 Firefox 连接 (127.0.0.1 :7055) - 2

      我使用的是Firefox版本36.0.1和Selenium-Webdrivergem版本2.45.0。我能够创建Firefox实例,但无法使用脚本继续进行进一步的操作无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055)错误。有人能帮帮我吗? 最佳答案 我遇到了同样的问题。降级到firefoxv33后一切正常。您可以找到旧版本here 关于ruby-无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055),我们在StackOverflow上找到一个类

    4. ruby - 检查字符串是否包含散列中的任何键并返回它包含的键的值 - 2

      我有一个包含多个键的散列和一个字符串,该字符串不包含散列中的任何键或包含一个键。h={"k1"=>"v1","k2"=>"v2","k3"=>"v3"}s="thisisanexamplestringthatmightoccurwithakeysomewhereinthestringk1(withspecialcharacterslike(^&*$#@!^&&*))"检查s是否包含h中的任何键的最佳方法是什么,如果包含,则返回它包含的键的值?例如,对于上面的h和s的例子,输出应该是v1。编辑:只有字符串是用户定义的。哈希将始终相同。 最佳答案

    5. ruby - Ruby 中的隐式返回值是怎么回事? - 2

      所以我开始关注ruby​​,很多东西看起来不错,但我对隐式return语句很反感。我理解默认情况下让所有内容返回self或nil但不是语句的最后一个值。对我来说,它看起来非常脆弱(尤其是)如果你正在使用一个不打算返回某些东西的方法(尤其是一个改变状态/破坏性方法的函数!),其他人可能最终依赖于一个返回对方法的目的并不重要,并且有很大的改变机会。隐式返回有什么意义?有没有办法让事情变得更简单?总是有返回以防止隐含返回被认为是好的做法吗?我是不是太担心这个了?附言当人们想要从方法中返回特定的东西时,他们是否经常使用隐式返回,这不是让你组中的其他人更容易破坏彼此的代码吗?当然,记录一切并给出

    6. ruby-on-rails - ruby 日期方程不返回预期的真值 - 2

      为什么以下不同?Time.now.end_of_day==Time.now.end_of_day-0.days#falseTime.now.end_of_day.to_s==Time.now.end_of_day-0.days.to_s#true 最佳答案 因为纳秒数不同:ruby-1.9.2-p180:014>(Time.now.end_of_day-0.days).nsec=>999999000ruby-1.9.2-p180:015>Time.now.end_of_day.nsec=>999999998

    7. ruby - 从 String#split 返回的零长度字符串 - 2

      在Ruby1.9.3(可能还有更早的版本,不确定)中,我试图弄清楚为什么Ruby的String#split方法会给我某些结果。我得到的结果似乎与我的预期相反。这是一个例子:"abcabc".split("b")#=>["a","ca","c"]"abcabc".split("a")#=>["","bc","bc"]"abcabc".split("c")#=>["ab","ab"]在这里,第一个示例返回的正是我所期望的。但在第二个示例中,我很困惑为什么#split返回零长度字符串作为返回数组的第一个值。这是什么原因呢?这是我所期望的:"abcabc".split("a")#=>["bc"

    8. ruby - 为什么 Integer.respond_to?( :even? ) 返回 false? - 2

      我一直在研究RubyKoans,我发现about_open_classes.rbkoan很有趣。特别是他们修改Integer#even?方法的最后一个测试。我想尝试一下这个概念,所以我打开了Irb并尝试运行Integer.respond_to?(:even?),但令我惊讶的是我得到了错误。然后我尝试了Fixnum.respond_to?(:even?)并得到了错误。我还尝试了Integer.respond_to?(:respond_to?)并得到了true,当我执行2.even?时,我也得到了true。我不知道发生了什么。谁能告诉我缺少什么? 最佳答案

    9. ruby - Ruby gsub 替换中的行为不一致? - 2

      两个gsub产生不同的结果。谁能解释一下为什么?代码也可在https://gist.github.com/franklsf95/6c0f8938f28706b5644d获得.ver=9999str="\tCFBundleDevelopmentRegion\n\ten\n\tCFBundleVersion\n\t0.1.190\n\tAppID\n\t000000000000000"putsstr.gsub/(CFBundleVersion\n\t.*\.).*()/,"#{$1}#{ver}#{$2}"puts'--------'putsstr.gsub/(CFBundleVersio

    10. ruby-on-rails - Ruby 中意外的大小写行为 - 2

      我在一段非常简单的代码(如我所想)中得到了一个错误的值:org=4caseorgwhenorg=4val='H'endputsval=>nil请不要生气,我希望我错过了一些非常明显的东西,但我真的想不通。谢谢。 最佳答案 这是典型的Ruby错误。case有两种被调用的方法,一种是你传递一个东西作为分支的基础,另一种是你不传递的东西。如果您确实在case中指定了一个表达式语句然后评估所有其他条件并与===进行比较.在这种情况下org评估为false和org===false显然不是真的。所有其他情况也是如此,它们要么是真的,要么是假的。

    随机推荐