【问题标题】:Why does flowing off the end of a non-void function without returning a value not produce a compiler error?为什么在不返回值的情况下从非 void 函数的末尾流出不会产生编译器错误?
【发布时间】:2010-12-09 06:19:21
【问题描述】:

自从我多年前意识到,默认情况下这不会产生错误(至少在 GCC 中),我一直想知道为什么?

我知道您可以发出编译器标志来产生警告,但它不应该总是错误吗?为什么非 void 函数没有返回值是有效的?

cmets 中要求的示例:

#include <stdio.h>
int stringSize()
{
}

int main()
{
    char cstring[5];
    printf( "the last char is: %c\n", cstring[stringSize()-1] ); 
    return 0;
}

...编译。

【问题讨论】:

  • 另外,我将所有警告都视为错误,无论多么微不足道,并且我会激活所有警告(如有必要,可以在本地停用......但在代码中很清楚为什么)。
  • -Werror=return-type 只会将该警告视为错误。我只是忽略了警告,而在追踪无效this 指针时的几分钟的挫败感将我带到了这里并得出了这个结论。
  • 更糟糕的是,从 std::optional 函数的末尾流出而不返回返回一个“真”可选
  • @Rufus 没必要。这正是你的机器/编译器/操作系统/月球周期发生的事情。由于未定义的行为,编译器生成的任何垃圾代码只是碰巧看起来像一个“真正的”可选,不管那是什么。
  • С++2003, 6.6.3 2:“从函数末尾流出相当于没有值的返回;这会导致返回值的函数出现未定义的行为”

标签: c++ c gcc g++


【解决方案1】:

C99 和 C++ 标准不要求函数返回值。值返回函数中缺少的 return 语句将仅在 main 函数中定义(以返回 0)。

理由包括检查每个代码路径是否返回一个值非常困难,并且可以使用嵌入式汇编程序或其他棘手的方法设置返回值。

来自C++11草稿:

§ 6.6.3/2

从函数末尾流出 [...] 会导致返回值的函数出现未定义的行为。

§ 3.6.1/5

如果控制到达main的末尾而没有遇到return语句,效果就是执行

return 0;

请注意,C++ 6.6.3/2 中描述的行为在 C 中是不一样的。


如果你用 -Wreturn-type 选项调用它,gcc 会给你一个警告。

-Wreturn-type 每当函数定义的返回类型为 默认为 int。也警告任何 没有返回值的 return 语句 在返回类型不是的函数中 无效(从末端掉落 函数体被认为是返回 没有价值),以及关于回报 在 a 中带有表达式的语句 返回类型为 void 的函数。

此警告由 -Wall 启用。


出于好奇,看看这段代码的作用:

#include <iostream>

int foo() {
   int a = 5;
   int b = a + 1;
}

int main() { std::cout << foo() << std::endl; } // may print 6

此代码具有正式未定义的行为,实际上它依赖于 calling convention 和 architecture。在一个特定的系统上,使用一个特定的编译器,返回值是最后一个表达式求值的结果,存储在该系统处理器的eax 寄存器中。

【讨论】:

  • 我会警惕将未定义的行为称为“允许”,尽管我承认将其称为“禁止”也是错误的。不是错误并且不需要诊断与“允许”并不完全相同。至少,您的回答读起来有点像您说可以这样做,但实际上并非如此。
  • @Catskul,你为什么买这个论点?识别函数的退出点并确保它们都返回一个值(以及声明的返回类型的值)不是很容易吗?
  • @Catskul,是的,不是的。静态类型和/或编译语言做了很多你可能认为“非常昂贵”的事情,但是因为它们只做一次,在编译时,它们的开销可以忽略不计。即使这样说,我也不明白为什么识别函数的退出点需要是超线性的:您只需遍历函数的 AST 并查找返回或退出调用。这是线性时间,非常有效。
  • @LightnessRacesinOrbit:如果一个有返回值的函数有时会立即返回一个值,有时调用另一个总是通过throw 或longjmp 退出的函数,编译器是否需要无法访问的return在调用非返回函数之后?不需要它的情况并不常见,即使在这种情况下也要求包含它可能不会很繁重,但不要求它的决定是合理的。
  • @supercat:超级智能编译器在这种情况下不会发出警告或错误,但是——再一次——这对于一般情况来说基本上是无法计算的,所以你会被一般规则所束缚拇指。但是,如果您知道永远不会到达函数结束,那么您与传统函数处理的语义相去甚远,是的,您可以继续执行此操作并知道它是安全的。坦率地说,在这一点上,你比 C++ 低一层,因此,它的所有保证无论如何都没有实际意义。
【解决方案2】:

gcc 默认情况下不会检查所有代码路径是否返回值,因为通常无法做到这一点。它假设你知道你在做什么。考虑一个使用枚举的常见示例:

Color getColor(Suit suit) {
    switch (suit) {
        case HEARTS: case DIAMONDS: return RED;
        case SPADES: case CLUBS:    return BLACK;
    }

    // Error, no return?
}

程序员知道,除非出现错误,否则此方法总是返回颜色。 gcc 相信您知道自己在做什么,因此它不会强迫您在函数底部添加 return。

另一方面,javac 会尝试验证所有代码路径是否都返回一个值,如果无法证明它们都返回值,则会抛出错误。此错误是 Java 语言规范规定的。请注意,有时它是错误的,您必须放入不必要的 return 语句。

char getChoice() {
    int ch = read();

    if (ch == -1 || ch == 'q') {
        System.exit(0);
    }
    else {
        return (char) ch;
    }

    // Cannot reach here, but still an error.
}

这是一个哲学差异。 C 和 C++ 是比 Java 或 C# 更宽容和信任的语言,因此新语言中的一些错误是 C/C++ 中的警告,并且默认情况下会忽略或关闭一些警告。

【讨论】:

  • 如果 javac 真的检查代码路径,它不会看到你永远无法到达那个点吗?
  • 在第一个中,它并没有给你覆盖所有枚举案例的功劳(你需要一个默认案例或在切换后返回),而在第二个中它不知道System.exit() 永远不会回来。
  • javac(一个强大的编译器)知道System.exit() 永远不会返回似乎很简单。我查了一下(java.sun.com/j2se/1.4.2/docs/api/java/lang/…),文档只是说它“永远不会正常返回”。我想知道这意味着什么......
  • @Paul:这意味着他们没有好的编辑器。所有其他语言都说“从不正常返回”——即“不使用正常返回机制返回”。
  • 我肯定更喜欢一个至少在遇到第一个示例时发出警告的编译器,因为如果有人向枚举添加新值,逻辑的正确性就会中断。我想要一个大声抱怨和/或崩溃(可能使用断言)的默认案例。
【解决方案3】:

您的意思是,为什么从返回值函数的末尾流出(即在没有显式 return 的情况下退出)不是错误?

首先,在 C 语言中,函数是否返回有意义的值仅在执行代码实际使用返回值时才至关重要。当您知道大多数时候无论如何都不会使用它时,也许该语言不想强迫您返回任何东西。

其次,显然语言规范不想强制编译器作者检测和验证所有可能的控制路径是否存在显式return(尽管在许多情况下这并不难做到)。此外,某些控制路径可能会导致非返回函数——编译器通常不知道的特征。这样的路径可能会成为令人讨厌的误报的来源。

另请注意,在这种情况下,C 和 C++ 对行为的定义不同。在 C++ 中,仅仅从返回值函数的末尾流出总是未定义的行为(无论调用代码是否使用函数的结果)。在 C 中,仅当调用代码尝试使用返回值时,才会导致未定义的行为。

【讨论】:

  • +1 但 C++ 不能从 main() 末尾省略 return 语句?
  • @Chris Lutz:是的,main 在这方面很特别。
【解决方案4】:

在 C/C++ 下,不从声称返回某些东西的函数返回是合法的。有很多用例,例如调用exit(-1),或者调用它或抛出异常的函数。

编译器不会拒绝合法的 C++,即使它会导致 UB,如果您要求它不要这样做。特别是,您要求生成 no warnings。 (默认情况下,Gcc 仍会打开一些,但添加后这些似乎与新功能一致,而不是旧功能的新警告)

更改默认的无参数 gcc 以发出一些警告可能是对现有脚本或 make 系统的重大更改。精心设计的 -Wall 处理警告,或切换个别警告。

学习使用 C++ 工具链是学习成为 C++ 程序员的障碍,但 C++ 工具链通常由专家编写并为专家编写。

【讨论】:

  • 是的,在我的Makefile 中,我使用-Wall -Wpedantic -Werror 运行它,但这是一个一次性的测试脚本,我忘记提供参数了。
  • 例如,将-Wduplicated-cond 变成-Wall 的一部分破坏了 GCC 引导程序。在大部分代码中看似合适的一些警告并不适用于所有代码。这就是默认情况下不启用它们的原因。
  • 您的第一句话似乎与接受的答案中的引用相矛盾“Flowing off .... undefined behavior...”。还是ub被认为是“合法的”?还是您的意思是它不是UB,除非实际使用(未)返回值?顺便说一句,我担心 C++ 案例
  • @tobi303 int foo() { exit(-1); } 不会从“声称返回 int”的函数返回 int。这在 C++ 中是合法的。现在,它不会返回 anything;永远不会到达该功能的终点。 实际上达到 foo 的结尾将是未定义的行为。忽略流程结束的情况,int foo() { throw 3.14; } 还声称返回 int 但从未返回。
  • 所以我猜void* foo(void* arg) { pthread_exit(NULL); } 也是出于同样的原因(当它的唯一用途是通过pthread_create(...,...,foo,...); 时)
【解决方案5】:

语言规则是,如果达到返回非void 值的函数的关闭} 并且调用者尝试使用该值,则行为未定义。只要调用者不使用该值,只要从函数末尾掉下就具有明确定义的行为。

可以要求所有可能的控制路径在离开函数之前执行return 语句,但 C 传统上不要求编译器进行这种代码分析。 (无论如何,许多编译器都会进行这种分析,并在适当的时候发出警告。)

允许非void 函数结束的主要原因是历史原因。 K&R C(Kernighan 和 Ritchie 的 1978 年第一版书中描述的版本,在 1989 年 ANSI 和 1990 ISO C 标准之前)没有void 关键字或类型。在 1999 年 ISO C 标准之前,C 有“隐式 int”规则,这意味着您可以声明或定义一个没有显式返回类型的函数,它会返回一个 int 结果。

在 K&R C 中,如果您想要一个不返回结果的函数,您可以在没有显式返回类型的情况下定义它并且根本不返回值:

#include <stdio.h>

do_something() {
    printf("Not returning a value\n");
}

int main() {
    do_something();
    return 0;
}

该函数实际上会返回一些垃圾 int 值,调用者会悄悄地忽略这些值。

在现代 C 中,你会这样写:

#include <stdio.h>

void do_something(void) {
    printf("Not returning a value\n");
}

int main(void) {
    do_something();
}

保证调用者不能尝试使用返回的值。从 C89/C90 开始,该语言仍然支持旧样式以避免破坏现有代码。当隐式 int 规则在 C99 中被删除时,对无法返回值的非void 函数的要求没有改变(并且大多数 C99 和更高版本的编译器仍然默认支持隐式 int 规则,可能带有警告,所以旧的 K&R C 代码仍然可以编译)。

【讨论】:

    【解决方案6】:

    我相信这是因为遗留代码(C 从来不需要 return 语句,C++ 也是如此)。可能有庞大的代码库依赖于该“功能”。但至少有-Werror=return-type 许多编译器(包括 gcc 和 clang)上的标志。

    【讨论】:

      【解决方案7】:

      在一些有限和罕见的情况下,在非 void 函数的末尾流出而不返回值可能很有用。就像下面的 MSVC 特定代码:

      double pi()
      {
          __asm fldpi
      }
      

      此函数使用 x86 汇编返回 pi。与 GCC 中的汇编不同,我不知道如何使用 return 来执行此操作而不涉及结果中的开销。

      据我所知,主流 C++ 编译器至少应该针对明显无效的代码发出警告。如果我将pi()的body设为空,GCC/Clang会报警告,MSVC会报错。

      人们在一些答案中提到了异常和exit。这些都不是正当理由。要么抛出异常,要么调用exit,不会使函数执行流到最后。编译器知道这一点:在 pi() 的空体中编写 throw 语句或调用 exit 将阻止编译器发出任何警告或错误。

      【讨论】:

      • MSVC 特别支持在内联 asm 在返回值寄存器中留下一个值后从非void 函数的末尾脱落。 (x87 st0 在这种情况下,EAX 表示整数。也许 xmm0 在调用约定中返回 xmm0 而不是 st0 中的 float/double)。定义此行为是特定于 MSVC 的;甚至不使用-fasm-blocks 来支持相同的语法使得这很安全。见Does __asm{}; return the value of eax?
      【解决方案8】:

      什么情况下不会报错?如果它声明了一个返回类型并且没有返回任何东西,这对我来说听起来像是一个错误。

      我能想到的一个例外是 main() 函数,它根本不需要 return 语句(至少在 C++ 中;我手头没有任何 C 标准)。如果没有返回,它会认为return 0; 是最后一个语句。

      【讨论】:

      • main() 需要 C 中的 return。
      • @Jefromi:OP 询问的是没有 return &lt;value&gt;; 语句的非 void 函数
      • main 在 C 和 C++ 中自动返回 0。 C89 需要显式返回。
      • @Chris:在 C99 中,main() 的末尾有一个隐含的 return 0;(仅限 main())。但无论如何添加return 0; 是一种很好的风格。
      【解决方案9】:

      我收到警告是因为我忘记添加语句 itr = itr -> 当前节点; 基本上缺少该语句,函数进入无限循环并且从未返回值,这就是我在编译期间收到该警告的原因

      void* list_get(list* lst, int idx){
      
          node* itr = lst->head;
          
          if (idx >= lst->size){
              printf("list out of index");
              exit(1);
          }
          
          while(itr != NULL){
              if(itr->index == idx){
                 return  itr->element;
              }
              itr = itr->currentNode;
           
          }
      
      
      }
      

      【讨论】:

        【解决方案10】:

        听起来您需要打开编译器警告:

        $ gcc -Wall -Wextra -Werror -x c -
        int main(void) { return; }
        cc1: warnings being treated as errors
        <stdin>: In function ‘main’:
        <stdin>:1: warning: ‘return’ with no value, in function returning non-void
        <stdin>:1: warning: control reaches end of non-void function
        $
        

        【讨论】:

        • 说“打开 -Werror”是没有答案的。很明显,分类为警告和错误的问题在严重性上存在差异,gcc 将这一类视为不太严重的类别。
        • @Jefromi:从纯语言的角度来看,警告和错误之间没有区别。编译器只需要发出“disgnostic message”。不需要停止编译或调用“错误”和其他“警告”。发出诊断消息(或任何类型)后,完全由您来决定。
        • 再一次,有问题的问题导致了 UB。编译器根本不需要捕捉 UB。
        • 在 6.9.1/12 的 n1256 中,它说“如果到达终止函数的 },并且调用者使用了函数调用的值,则行为未定义。”跨度>
        • @Chris Lutz:我没看到。在非 void 函数中使用 explicit 空 return; 是违反约束的,在 void 函数中使用 return &lt;value&gt;; 也是违反约束的。但那是,我相信,不是主题。据我了解,OP 是关于在没有 return 的情况下退出非 void 函数(仅允许控制从函数末尾流出)。这不是违反约束,AFAIK。标准只是说它始终是 C++ 中的 UB,有时是 C 中的 UB。
        【解决方案11】:

        这在 c99 中是违反约束的,但在 c89 中不是。对比:

        c89:

        3.6.6.4 return 声明

        约束

        带有表达式的return 语句不应出现在 返回类型为void的函数。

        c99:

        6.8.6.4 return 声明

        约束

        带有表达式的return 语句不得出现在返回类型为void 的函数中。没有表达式的return 语句只能出现在返回类型为void 的函数中。

        即使在--std=c99 模式下,gcc 也只会抛出一个警告(尽管不需要启用额外的-W 标志,这是默认或 c89/90 中的要求)。

        编辑在c89中添加,“到达终止函数的}相当于 在没有表达式的情况下执行return 语句”(3.6.6.4)。但是,在 c99 中,行为未定义(6.9.1)。

        【讨论】:

        • 请注意,这仅涵盖显式 return 语句。它不包括在不返回值的情况下从函数末尾脱落。
        • 请注意,C99 错过了“到达终止函数的 } 等同于执行没有表达式的 return 语句”,因此它没有违反约束,因此不需要诊断。
        猜你喜欢
        • 1970-01-01
        • 2011-09-25
        • 1970-01-01
        • 1970-01-01
        • 2017-10-10
        • 1970-01-01
        • 2013-06-28
        相关资源
        最近更新 更多