为什么从非空函数的末尾流出而不返回值不会产生编译器错误?


158

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

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

评论中要求的示例:

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

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

...编译。


9
另外,我将所有警告视为微小的错误,然后将其激活(如果需要,可以通过本地取消激活...,但是在代码中清楚为什么),就可以激活所有警告。
Matthieu M.

8
-Werror=return-type只会将该警告视为错误。我只是忽略了警告,而经过几分钟的挫败追寻无效this指针导致我到了这里并得出了这个结论。
jozxyqk

由于从std::optional函数的结尾流出而不返回返回“ true”可选事实,这使情况变得更糟
Rufus,

@Rufus不必。那就是在您的机器/编译器/ OS /阴历周期上发生的事情。无论编译器由于未定义行为而生成的任何垃圾代码,恰好看起来都是“ true”可选的,无论是什么。
underscore_d

Answers:


146

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

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

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-type定义函数时发出警告。还警告函数中返回类型不为void(没有返回值的函数类型的末尾被认为是没有值的返回)的函数中没有返回值的任何return语句,并警告函数中具有表达式的return语句返回类型为空。

该警告由-Wall启用。


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

#include <iostream>

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

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

该代码具有形式上未定义的行为,在实践中,它称为依赖于约定体系结构。在一个具有特定编译器的特定系统上,返回值是最后一个表达式求值的结果,存储在该eax系统处理器的寄存器中。


13
我会谨慎地将未定义的行为称为“允许的”,尽管承认我也将其称为“禁止的”是错误的。不是错误且不需要诊断与“允许”并不完全相同。至少,您的答案读起来有点像您在说可以做,但实际上不是。
Lightness Races in Orbit

4
@Catskul,为什么要买那个论点?识别一个函数的退出点并确保它们都返回一个值(以及声明的返回类型的值)是否可行(如果不是很简单)?
BlueBomber

3
@Catskul,是和不是。静态类型化和/或编译后的语言执行了许多您可能认为“过高的成本”的工作,但是由于它们仅在编译时执行一次,因此费用可忽略不计。即使这么说,我也不明白为什么识别一个函数的退出点需要是超线性的:您只需遍历该函数的AST并查找返回或退出调用。那是线性时间,绝对有效。
BlueBomber

3
@LightnessRacesinOrbit:如果具有返回值的函数有时会立即返回一个值,有时会调用另一个始终通过throw或退出的函数longjmp,那么return在调用非返回函数之后,编译器是否需要一个无法访问的函数?不需要它的情况不是很常见,即使在这种情况下也要求包含它可能不会很繁琐,但是不要求它的决定是合理的。
超级猫

1
@supercat:在这种情况下,超级智​​能编译器不会发出警告或错误,但是-再次-这对于一般情况而言基本上是无法计算的,因此您必须牢记一般的经验法则。但是,如果您知道永远无法达到功能终止,那么您与传统函数处理的语义相距甚远,可以的话,您可以继续进行下去,并知道它是安全的。坦白说,那时您是C ++的下一层,因此,无论如何,它的所有保证都没有意义。

42

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

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

    // Error, no return?
}

您程序员知道,除非存在错误,否则此方法始终返回颜色。gcc相信您知道自己在做什么,因此不会强迫您在函数底部放一个返回值。

另一方面,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.
}

这是哲学上的差异。与Java或C#相比,C和C ++是更宽松的语言和更受信任的语言,因此,较新语言中的某些错误是C / C ++中的警告,默认情况下会忽略或关闭某些警告。


2
如果javac实际检查了代码路径,难道不是看不到那个点吗?
克里斯·卢兹

3
在第一个案例中,它不能为您涵盖所有枚举情况(您需要默认案例或在切换后返回),而在第二个案例中,它不知道System.exit()永远不会返回。
约翰·库格曼

2
对于javac(一个本来功能强大的编译器)来说,知道System.exit()永不返回似乎很简单。我了一下(java.sun.com/j2se/1.4.2/docs/api/java/lang/…),文档只是说它“从不正常返回”。我想知道这是什么意思……
Paul Biggar

@Paul:这意味着他们没有好编辑。所有其他语言都说“从不正常返回”-即“不使用正常返回机制返回”。
Max Lybbert 09年

1
我绝对希望编译器至少在遇到第一个示例时发出警告,因为如果有人向枚举添加新值,则逻辑的正确性将被破坏。我想要一个大声抱怨和/或崩溃的默认案例(可能使用断言)。
Injektilo

14

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

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

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

还要注意,在这种情况下,C和C ++在行为方面的定义不同。在C ++中,仅从值返回函数的末尾流出始终是未定义的行为(无论调用代码是否使用了函数的结果)。在C语言中,仅当调用代码尝试使用返回的值时,这才导致未定义的行为。


+1,但C ++不能return从末尾省略语句main()吗?
克里斯·卢兹

3
@Chris Lutz:是的,main在这方面很特别。
AnT

5

在C / C ++中,合法的做法是不要从声称要返回某些内容的函数中返回。有许多用例,例如调用exit(-1),或者调用它或引发异常的函数。

即使您要求不这样做,编译器也不会拒绝合法的C ++,即使它导致了UB。特别是,您不要求生成任何警告。(默认情况下,Gcc仍会启用某些功能,但添加时似乎与新功能保持一致,而不是对旧功能的新警告)

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

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


是的,我用来Makefile运行它-Wall -Wpedantic -Werror,但这是一个一次性测试脚本,我忘了提供参数。
Fund Monica的诉讼

2
作为一个例子,使得-Wduplicated-cond部分-Wall破产GCC引导。某些警告似乎在大多数代码中并不适用于所有代码。这就是为什么默认情况下不启用它们。
哦,有人需要小菜一碟

您的第一句话似乎与被接受的答案“流淌……未定义的行为……”中的引用相矛盾。还是ub被视为“合法”?还是说除非实际使用(非)返回值,否则它不是UB?我担心C ++案件顺便说一句
idclev 463035818

@ tobi303 int foo() { exit(-1); }不会int从“声明返回int”的函数返回。这在C ++中是合法的。现在,它不会返回任何东西 ; 该功能的末尾永远不会到达。 实际上到达末尾foo将是未定义的行为。忽略流程结束的情况,int foo() { throw 3.14; }也声称要返回,int但永远不会返回。
Yakk-Adam Nevraumont

1
所以我想void* foo(void* arg) { pthread_exit(NULL); }出于同样的原因(当它的唯一用法是通过时pthread_create(...,...,foo,...);
也很好

1

我认为这是由于遗留代码(C从未要求return语句,C ++也不是)。可能有大量依赖此“功能”的代码库。但是至少-Werror=return-type 在许多编译器(包括gcc和clang)上都有标记。


1

在某些有限且罕见的情况下,从非void函数的末尾流出而不返回值可能很有用。类似于以下特定于MSVC的代码:

double pi()
{
    __asm fldpi
}

此函数使用x86汇编返回pi。与GCC中的汇编不同,在return不涉及结果开销的情况下,我不知道如何使用此方法。

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

人们提到了例外和exit一些答案。这些不是正当理由。无论是抛出一个异常,或调用exit,会不会使函数执行流走到底。而且编译器知道这一点:编写throw语句或调用exit空的主体pi()将停止编译器发出的任何警告或错误。


在内void联汇编将值保留在返回值寄存器中之后,MSVC特别支持从非函数末尾退出。(st0在这种情况下为x87 ,EAX为整数。也许在调用约定中为xmm0,它在xmm0中返回float / double而不是st0)。定义此行为特定于MSVC。甚至不-fasm-blocks支持相同语法的clang 也很安全。请参见__asm {}吗?返回eax的值?
彼得·科德斯

0

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

我能想到的一个例外是main()函数,它根本不需要return声明(至少在C ++中;我没有任何一种C标准)。如果没有返回,它将像return 0;最后一条语句一样工作。


1
main()需要return在C.
克里斯·卢茨

@Jefromi:OP正在询问一个没有return <value>;声明的非void函数
法案

2
main在C和C ++中自动返回0。C89需要明确的回报。
Johannes Schaub-litb

1
@Chris:在C99中return 0;main()main()仅)末尾有一个隐式。但是return 0;无论如何添加它是一个很好的样式。
09年

0

听起来您需要提高编译器警告:

$ 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
$

5
说“打开-Werror”是无法回答的。显然,在被分类为警告和错误的问题之间,严重程度存在差异,并且gcc将此问题视为较不严重的问题。
卡斯卡贝尔

2
@Jefromi:从纯语言的角度来看,警告和错误之间没有区别。仅要求编译器发出“诊断消息”。无需停止编译或将其称为“错误”而将其称为“警告”。发出(或任何一种)诊断消息,这完全取决于您做出决定。
AnT

1
再一次,有问题的问题导致UB。编译器根本不需要捕获UB。
AnT

1
在n1256中的6.9.1 / 12中,它说:“如果到达终止函数的},并且调用者使用了函数调用的值,则该行为未定义。”
Johannes Schaub-litb

2
@克里斯·卢茨:我没看到。在非void函数中使用显式空值return;是一种约束违规,而return <value>;在void函数中使用是一种约束违规。但是,我认为这不是主题。据我所知,OP是关于退出一个没有void的非void函数return(只是允许控件从该函数的末尾流出)。这不是违反约束条件,即AFAIK。该标准只是说它始终是C ++中的UB,有时甚至是C中的
UB。– AnT

-2

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

c89:

3.6.6.4 return声明

约束条件

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

c99:

6.8.6.4 return声明

约束条件

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

即使在--std=c99模式下,gcc也只会发出警告(尽管不需要启用其他-W标志,这是默认情况下或c89 / 90中所要求的)。

编辑以在c89中添加,“到达}终止函数的功能等同于执行return不带表达式的语句”(3.6.6.4)。但是,在c99中,行为是不确定的(6.9.1)。


4
请注意,这仅涵盖显式return语句。它不涵盖在不返回值的情况下从函数末尾掉落的情况。
约翰·库格曼

3
请注意,C99缺少“到达终止函数的}等效于执行不带表达式的return语句”,因此它没有违反约束,因此不需要诊断。
Johannes Schaub-litb
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.