Warum führt das Abfließen des Endes einer nicht ungültigen Funktion ohne Rückgabe eines Werts nicht zu einem Compilerfehler?

158

Seit ich vor vielen Jahren festgestellt habe, dass dies standardmäßig keinen Fehler erzeugt (zumindest in GCC), habe ich mich immer gefragt, warum?

Ich verstehe, dass Sie Compiler-Flags ausgeben können, um eine Warnung zu erzeugen, aber sollte es nicht immer ein Fehler sein? Warum ist es sinnvoll, dass eine nicht ungültige Funktion, die keinen Wert zurückgibt, gültig ist?

Ein Beispiel wie in den Kommentaren angefordert:

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

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

... kompiliert.

Catskul
quelle
9
Alternativ behandle ich alle Warnungen, wie trivial sie auch sein mögen, wie Fehler und aktiviere alle Warnungen, die ich kann (ggf. mit lokaler Deaktivierung ... aber dann ist im Code klar, warum).
Matthieu M.
8
-Werror=return-typebehandelt nur diese Warnung als Fehler. Ich habe die Warnung einfach ignoriert und die paar Minuten der Frustration, die einen ungültigen thisZeiger aufspürten, führten mich hierher und zu dieser Schlussfolgerung.
jozxyqk
Dies wird durch die Tatsache verschlimmert, dass das Abfließen des Endes einer std::optionalFunktion ohne Rückgabe ein "wahres" optionales Ergebnis zurückgibt
Rufus
@ Rufus Es muss nicht. Genau das ist auf Ihrem Computer- / Compiler- / Betriebssystem- / Mondzyklus passiert. Unabhängig davon, welcher Junk-Code der Compiler aufgrund des undefinierten Verhaltens generiert hat, sieht er wie eine "echte" Option aus, was auch immer das ist.
underscore_d

Antworten:

146

C99- und C ++ - Standards erfordern keine Funktionen, um einen Wert zurückzugeben. Die fehlende return-Anweisung in einer Funktion zur Rückgabe von Werten wird 0nur in der definiert (um zurückzugeben )main Funktion .

Das Grundprinzip beinhaltet, dass es ziemlich schwierig ist zu überprüfen, ob jeder Codepfad einen Wert zurückgibt, und dass ein Rückgabewert mit eingebettetem Assembler oder anderen kniffligen Methoden festgelegt werden kann.

Aus C ++ 11 Entwurf:

§ 6.6.3 / 2

Das Abfließen des Endes einer Funktion [...] führt zu einem undefinierten Verhalten in einer Funktion, die Werte zurückgibt.

§ 3.6.1 / 5

Wenn die Kontrolle das Ende erreicht, mainohne auf eine return Anweisung zu stoßen , bewirkt dies die Ausführung

return 0;

Beachten Sie, dass das in C ++ 6.6.3 / 2 beschriebene Verhalten in C nicht dasselbe ist.


gcc gibt eine Warnung aus, wenn Sie es mit der Option -Wreturn-type aufrufen.

-Wreturn-Typ Warnen, wenn eine Funktion mit einem Rückgabetyp definiert wird, der standardmäßig int ist. Warnen Sie auch vor einer return-Anweisung ohne Rückgabewert in einer Funktion, deren Rückgabetyp nicht ungültig ist (das Abfallen vom Ende des Funktionskörpers wird als Rückgabe ohne Wert betrachtet), und vor einer return-Anweisung mit einem Ausdruck in einer Funktion, deren Rückgabetyp ist ungültig.

Diese Warnung wird von -Wall aktiviert .


Schauen Sie sich aus Neugier an, was dieser Code bewirkt:

#include <iostream>

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

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

Dieser Code hat ein formal undefiniertes Verhalten und wird in der Praxis als konventions- und architekturabhängig bezeichnet . Auf einem bestimmten System mit einem bestimmten Compiler ist der Rückgabewert das Ergebnis der letzten Ausdrucksauswertung, die im eaxRegister des Prozessors dieses Systems gespeichert ist .

fnieto - Fernando Nieto
quelle
13
Ich würde mich davor hüten, undefiniertes Verhalten als "erlaubt" zu bezeichnen, obwohl ich zugegebenermaßen auch falsch wäre, es als "verboten" zu bezeichnen. Kein Fehler zu sein und keine Diagnose zu erfordern, ist nicht ganz dasselbe wie "erlaubt". Zumindest lautet Ihre Antwort ein bisschen so, als würden Sie sagen, dass dies in Ordnung ist, was im Großen und Ganzen nicht der Fall ist.
Leichtigkeitsrennen im Orbit
4
@Catskul, warum kaufst du dieses Argument? Wäre es nicht machbar, wenn nicht einfach, Ausgangspunkte einer Funktion zu identifizieren und sicherzustellen, dass alle einen Wert (und einen Wert des deklarierten Rückgabetyps) zurückgeben?
BlueBomber
3
@Catskul, ja und nein. Statisch typisierte und / oder kompilierte Sprachen machen viele Dinge, die Sie wahrscheinlich als "unerschwinglich teuer" betrachten würden, aber weil sie es nur einmal tun, haben sie zur Kompilierungszeit vernachlässigbare Kosten. Trotzdem verstehe ich nicht, warum das Identifizieren von Austrittspunkten einer Funktion superlinear sein muss: Sie durchlaufen einfach den AST der Funktion und suchen nach Rück- oder Ausgangsaufrufen. Das ist lineare Zeit, die ausgesprochen effizient ist.
BlueBomber
3
@LightnessRacesinOrbit: Wenn eine Funktion mit einem Rückgabewert manchmal sofort mit einem Wert zurückgibt und manchmal eine andere Funktion aufruft, die immer über throwoder beendet wird longjmp, sollte der Compiler returnnach dem Aufruf der nicht zurückkehrenden Funktion eine nicht erreichbare Funktion benötigen ? Der Fall, in dem es nicht benötigt wird, ist nicht sehr häufig, und eine Anforderung, es auch in solchen Fällen aufzunehmen, wäre wahrscheinlich nicht lästig gewesen, aber die Entscheidung, es nicht zu verlangen, ist vernünftig.
Supercat
1
@supercat: Ein superintelligenter Compiler wird in einem solchen Fall weder warnen noch Fehler machen, aber auch dies ist für den allgemeinen Fall im Wesentlichen nicht kalkulierbar, sodass Sie an einer allgemeinen Faustregel festhalten. Wenn Sie jedoch wissen, dass Ihr Funktionsende niemals erreicht wird, sind Sie so weit von der Semantik der traditionellen Funktionshandhabung entfernt, dass Sie dies tun und wissen können, dass es sicher ist. Ehrlich gesagt sind Sie zu diesem Zeitpunkt eine Schicht unter C ++ und als solche sind alle Garantien ohnehin umstritten.
Leichtigkeitsrennen im Orbit
42

gcc überprüft standardmäßig nicht, ob alle Codepfade einen Wert zurückgeben, da dies im Allgemeinen nicht möglich ist. Es setzt voraus, dass Sie wissen, was Sie tun. Betrachten Sie ein allgemeines Beispiel mit Aufzählungen:

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

    // Error, no return?
}

Sie als Programmierer wissen, dass diese Methode, abgesehen von einem Fehler, immer eine Farbe zurückgibt. gcc vertraut darauf, dass Sie wissen, was Sie tun, sodass Sie nicht gezwungen sind, am Ende der Funktion eine Rückgabe zu setzen.

Javac hingegen versucht zu überprüfen, ob alle Codepfade einen Wert zurückgeben, und gibt einen Fehler aus, wenn nicht nachgewiesen werden kann, dass alle dies tun. Dieser Fehler wird durch die Java-Sprachspezifikation vorgeschrieben. Beachten Sie, dass es manchmal falsch ist und Sie eine unnötige return-Anweisung eingeben müssen.

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

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

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

Es ist ein philosophischer Unterschied. C und C ++ sind freizügigere und vertrauenswürdigere Sprachen als Java oder C #. Daher sind einige Fehler in den neueren Sprachen Warnungen in C / C ++, und einige Warnungen werden standardmäßig ignoriert oder deaktiviert.

John Kugelman
quelle
2
Wenn javac tatsächlich Codepfade überprüft, würde es dann nicht sehen, dass Sie diesen Punkt niemals erreichen könnten?
Chris Lutz
3
Im ersten Fall erhalten Sie keine Gutschrift für die Abdeckung aller Enum-Fälle (Sie benötigen einen Standardfall oder eine Rückgabe nach dem Wechsel), und im zweiten Fall weiß es nicht, dass System.exit()niemals zurückgegeben wird.
John Kugelman
2
Für javac (einen ansonsten leistungsstarken Compiler) scheint es unkompliziert zu sein, zu wissen, dass er System.exit()niemals zurückkehrt. Ich habe es nachgeschlagen ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ) und die Dokumente sagen nur, dass es "normalerweise nie zurückkehrt". Ich frage mich, was das bedeutet ...
Paul Biggar
@ Paul: Es bedeutet, dass sie keinen guten Redakteur hatten. Alle anderen Sprachen sagen "kehrt nie normal zurück" - dh "kehrt nicht mit dem normalen Rückgabemechanismus zurück".
Max Lybbert
1
Ich würde definitiv einen Compiler bevorzugen, der zumindest warnt, wenn er auf dieses erste Beispiel stößt, da die Richtigkeit der Logik brechen würde, wenn jemand der Aufzählung einen neuen Wert hinzufügt. Ich möchte einen Standardfall, der sich lautstark beschwert und / oder abgestürzt ist (wahrscheinlich unter Verwendung einer Behauptung).
Injektilo
14

Sie meinen, warum das Ende einer Wertrückgabefunktion abfließen (dh ohne explizite Beendigung beenden) return ) kein Fehler ist?

Erstens ist in C nur dann entscheidend, ob eine Funktion etwas Sinnvolles zurückgibt oder nicht, wenn der ausführende Code tatsächlich ausgeführt wird verwendet den zurückgegebenen Wert. Vielleicht wollte die Sprache Sie nicht zwingen, etwas zurückzugeben, wenn Sie wissen, dass Sie es die meiste Zeit sowieso nicht verwenden werden.

Zweitens wollte die Sprachspezifikation die Compilerautoren anscheinend nicht zwingen, alle möglichen Steuerpfade auf das Vorhandensein eines expliziten zu erkennen und zu überprüfen return(obwohl dies in vielen Fällen nicht so schwierig ist). Einige Steuerpfade können auch zu nicht zurückkehrenden Funktionen führen - dem Merkmal, das dem Compiler im Allgemeinen nicht bekannt ist. Solche Pfade können zu ärgerlichen Fehlalarmen werden.

Beachten Sie auch, dass sich C und C ++ in ihren Definitionen des Verhaltens in diesem Fall unterscheiden. In C ++ ist das Abfließen des Endes einer Wertrückgabefunktion immer ein undefiniertes Verhalten (unabhängig davon, ob das Ergebnis der Funktion vom aufrufenden Code verwendet wird). In C führt dies nur dann zu undefiniertem Verhalten, wenn der aufrufende Code versucht, den zurückgegebenen Wert zu verwenden.

Ameise
quelle
+1, aber kann C ++ keine returnAnweisungen am Ende von weglassen main()?
Chris Lutz
3
@ Chris Lutz: Ja, mainist in dieser Hinsicht etwas Besonderes.
Am
5

Unter C / C ++ ist es legal, nicht von einer Funktion zurückzukehren, die behauptet, etwas zurückzugeben. Es gibt eine Reihe von Anwendungsfällen, z. B. Anrufeexit(-1) oder eine Funktion, die es aufruft oder eine Ausnahme auslöst.

Der Compiler wird legales C ++ nicht ablehnen, selbst wenn es zu UB führt, wenn Sie dies nicht möchten. Insbesondere bitten Sie um keine Warnungen generiert werden. (Gcc aktiviert einige standardmäßig immer noch, aber wenn sie hinzugefügt werden, scheinen diese mit neuen Funktionen übereinzustimmen, nicht mit neuen Warnungen für alte Funktionen.)

Das Ändern des Standard-No-Arg-gcc, um einige Warnungen auszugeben, kann eine wichtige Änderung für vorhandene Skripte oder Make-Systeme sein. Auch gut gestaltete-Wall und behandeln Warnungen oder schalten einzelne Warnungen um.

Das Erlernen der Verwendung einer C ++ - Toolkette ist ein Hindernis für das Erlernen eines C ++ - Programmierers. C ++ - Toolketten werden jedoch normalerweise von und für Experten geschrieben.

Yakk - Adam Nevraumont
quelle
Ja, in meinem Makefileläuft es -Wall -Wpedantic -Werror, aber dies war ein einmaliges Testskript, für das ich vergessen habe, die Argumente anzugeben.
Fund Monica Klage
2
Beispiel: Ein -Wduplicated-condTeil des -Wallkaputten GCC-Bootstraps. Einige Warnungen, die in den meisten Codes angemessen erscheinen, sind nicht in allen Codes angemessen. Deshalb sind sie nicht standardmäßig aktiviert.
Oh, jemand braucht einen Puppen
Ihr erster Satz scheint im Widerspruch zu dem Zitat in der akzeptierten Antwort "Abfließen ... undefiniertes Verhalten ..." zu stehen. Oder gilt ub als "legal"? Oder meinen Sie, dass es nicht UB ist, es sei denn, der (nicht) zurückgegebene Wert wird tatsächlich verwendet? Ich mache mir Sorgen um den C ++ - Fall übrigens
idclev 463035818
@ tobi303 int foo() { exit(-1); }kehrt nicht intvon einer Funktion zurück, die "behauptet, int zurückzugeben". Dies ist in C ++ legal. Jetzt gibt es nichts zurück ; Das Ende dieser Funktion wird nie erreicht. Das Ende tatsächlich zu erreichen,foo wäre undefiniertes Verhalten. Das Ignorieren von Fällen am Ende des Prozesses int foo() { throw 3.14; }behauptet ebenfalls, zurückzukehren int, tut dies jedoch nie.
Yakk - Adam Nevraumont
1
Ich denke, es void* foo(void* arg) { pthread_exit(NULL); }ist aus dem gleichen Grund in Ordnung (wenn seine einzige Verwendung über ist pthread_create(...,...,foo,...);)
idclev 463035818
1

Ich glaube, das liegt an altem Code (C hat nie eine return-Anweisung benötigt, C ++ auch nicht). Es gibt wahrscheinlich eine riesige Codebasis, die sich auf diese "Funktion" stützt. Aber zumindest gibt es -Werror=return-type auf vielen Compilern (einschließlich gcc und clang) eine Flagge.

d.lozinski
quelle
1

In einigen begrenzten und seltenen Fällen kann es nützlich sein, das Ende einer nicht leeren Funktion zu verlassen, ohne einen Wert zurückzugeben. Wie der folgende MSVC-spezifische Code:

double pi()
{
    __asm fldpi
}

Diese Funktion gibt pi mithilfe der x86-Assembly zurück. Im Gegensatz zur Montage in GCC kenne ich keine Verwendungsmöglichkeitreturn zu tun, ohne Overhead in das Ergebnis einzubeziehen.

Soweit ich weiß, sollten Mainstream-C ++ - Compiler zumindest Warnungen für scheinbar ungültigen Code ausgeben. Wenn ich den Körper aus machepi() leer mache, meldet GCC / Clang eine Warnung und MSVC meldet einen Fehler.

Die Leute erwähnten Ausnahmen und exitin einigen Antworten. Das sind keine triftigen Gründe. Durch das Auslösen einer Ausnahme oder das Aufrufen exitwird die Funktionsausführung nicht am Ende ausgeführt. Und die Compiler wissen es: Wenn Sie eine throw-Anweisung schreiben oder exitden leeren Body von aufrufen, pi()werden alle Warnungen oder Fehler eines Compilers gestoppt.

Yongwei Wu
quelle
MSVC unterstützt speziell das Abfallen des Endes einer Nichtfunktion void, nachdem inline asm einen Wert im Rückgaberegister belassen hat . (x87 st0in diesem Fall EAX für Ganzzahl. Und möglicherweise xmm0 in einer aufrufenden Konvention, die float / double in xmm0 anstelle von st0 zurückgibt). Das Definieren dieses Verhaltens ist spezifisch für MSVC. Nicht einmal das Klirren -fasm-blocks, um die gleiche Syntax zu unterstützen, macht dies sicher. Siehe Does __asm ​​{}; den Wert von eax zurückgeben?
Peter Cordes
0

Unter welchen Umständen erzeugt es keinen Fehler? Wenn es einen Rückgabetyp deklariert und nichts zurückgibt, klingt es für mich wie ein Fehler.

Die einzige Ausnahme, an die ich denken kann, ist die main()Funktion, für die überhaupt keine returnAnweisung erforderlich ist (zumindest in C ++; ich habe keinen der C-Standards zur Hand). Wenn es keine Rückgabe gibt, verhält es sich so, als wäre return 0;es die letzte Anweisung.

David Thornley
quelle
1
main()braucht eine returnin C.
Chris Lutz
@Jefromi: Das OP fragt nach einer nicht ungültigen Funktion ohne return <value>;Aussage
Bill
2
main gibt in C und C ++ automatisch 0 zurück. C89 benötigt eine explizite Rückgabe.
Johannes Schaub - Litb
1
@ Chris: In C99 gibt es ein implizites return 0;am Ende von main()(und main()nur). Aber es ist return 0;trotzdem ein guter Stil, etwas hinzuzufügen .
PMG
0

Klingt so, als müssten Sie Ihre Compiler-Warnungen aufdrehen:

$ 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
$
Chris Lutz
quelle
5
Das Sagen von "Einschalten - Fehler" ist keine Antwort. Es gibt eindeutig einen Unterschied in der Schwere zwischen Problemen, die als Warnungen und Fehler eingestuft werden, und gcc behandelt dieses Problem als die weniger schwerwiegende Klasse.
Cascabel
2
@Jefromi: Aus rein sprachlicher Sicht gibt es keinen Unterschied zwischen Warnungen und Fehlern. Der Compiler muss nur eine "disgnostische Nachricht" ausgeben. Es ist nicht erforderlich, die Kompilierung zu beenden oder etwas als "Fehler" und etwas anderes als "Warnung" zu bezeichnen. Wenn eine Diagnosemeldung (oder eine andere Art) ausgegeben wird, liegt es ganz bei Ihnen, eine Entscheidung zu treffen.
Am
1
Andererseits verursacht das fragliche Problem UB. Compiler müssen UB überhaupt nicht fangen.
Am
1
In 6.9.1 / 12 in n1256 heißt es: "Wenn das}, das eine Funktion beendet, erreicht ist und der Wert des Funktionsaufrufs vom Aufrufer verwendet wird, ist das Verhalten undefiniert."
Johannes Schaub - Litb
2
@ Chris Lutz: Ich sehe es nicht. Es ist eine Einschränkungsverletzung, ein explizites Leerzeichen return;in einer nicht ungültigen Funktion zu verwenden, und es ist eine Einschränkungsverletzung, die return <value>;in einer ungültigen Funktion verwendet wird. Aber das ist, glaube ich, nicht das Thema. Beim OP geht es, wie ich es verstanden habe, darum, eine nicht leere Funktion ohne a zu verlassen return(nur die Kontrolle über das Ende der Funktion abfließen zu lassen). Es ist keine Einschränkungsverletzung, AFAIK. Der Standard sagt nur, dass es immer UB in C ++ und manchmal UB in C ist.
AnT
-2

Es ist eine Einschränkungsverletzung in c99, aber nicht in c89. Kontrast:

c89:

3.6.6.4 Die returnAussage

Einschränkungen

EIN return Anweisung mit einem Ausdruck darf nicht in einer Funktion erscheinen, deren Rückgabetyp ist void.

c99:

6.8.6.4 Die return Aussage

Einschränkungen

Eine returnAnweisung mit einem Ausdruck darf nicht in einer Funktion erscheinen, deren Rückgabetyp ist void. Eine returnAnweisung ohne Ausdruck darf nur in einer Funktion erscheinen, deren Rückgabetyp ist void.

Selbst im --std=c99Modus gibt gcc nur eine Warnung aus (obwohl keine zusätzlichen -WFlags aktiviert werden müssen, wie dies standardmäßig oder in c89 / 90 erforderlich ist).

Bearbeiten Sie, um hinzuzufügen, dass in c89 "das Erreichen desjenigen, der }eine Funktion beendet, dem Ausführen einer returnAnweisung ohne Ausdruck entspricht" (3.6.6.4). In c99 ist das Verhalten jedoch undefiniert (6.9.1).

Matt B.
quelle
4
Beachten Sie, dass dies nur explizite returnAussagen abdeckt . Es deckt nicht das Herunterfallen am Ende einer Funktion ab, ohne einen Wert zurückzugeben.
John Kugelman
3
Beachten Sie, dass C99 "das Erreichen des}, mit dem eine Funktion beendet wird, nicht der Ausführung einer return-Anweisung ohne Ausdruck entspricht", sodass keine Einschränkungsverletzung vorliegt und daher keine Diagnose erforderlich ist.
Johannes Schaub - Litb