Dlaczego wypływ z końca funkcji innej niż void bez zwracania wartości nie powoduje błędu kompilatora?

158

Odkąd wiele lat temu zdałem sobie sprawę, że domyślnie nie powoduje to błędu (przynajmniej w GCC), zawsze zastanawiałem się, dlaczego?

Rozumiem, że możesz wysyłać flagi kompilatora, aby wygenerować ostrzeżenie, ale czy nie powinno to zawsze być błędem? Dlaczego ma sens, aby funkcja nieważna, która nie zwraca wartości, była prawidłowa?

Przykład zgodnie z prośbą w komentarzach:

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

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

... kompiluje się.

Catskul
źródło
9
Alternatywnie traktuję wszystkie ostrzeżenia, jakkolwiek trywialne, jak błędy, i aktywuję wszystkie ostrzeżenia, które mogę (z lokalną dezaktywacją, jeśli to konieczne ... ale wtedy jest jasne, dlaczego).
Matthieu M.
8
-Werror=return-typepotraktuje to ostrzeżenie jako błąd. Właśnie zignorowałem ostrzeżenie, a kilka minut frustracji w poszukiwaniu nieprawidłowego thiswskaźnika doprowadziło mnie tutaj i do tego wniosku.
jozxyqk
Sytuację pogarsza fakt, że wypływ z końca std::optionalfunkcji bez zwracania zwraca „true” opcjonalne
Rufus
@Rufus To nie musi. To właśnie wydarzyło się na twoim komputerze / kompilatorze / systemie operacyjnym / cyklu księżycowym. Jakikolwiek niepotrzebny kod, który kompilator wygenerował z powodu nieokreślonego zachowania, po prostu wyglądał jak „prawdziwy” opcjonalny, cokolwiek to jest.
underscore_d

Odpowiedzi:

146

Standardy C99 i C ++ nie wymagają funkcji do zwracania wartości. Brakująca instrukcja return w funkcji zwracającej wartość zostanie zdefiniowana (do zwrócenia 0) tylko w mainfunkcji.

Uzasadnienie obejmuje stwierdzenie, że sprawdzenie, czy każda ścieżka kodu zwraca wartość, jest dość trudne, a wartość zwracaną można ustawić za pomocą wbudowanego asemblera lub innych skomplikowanych metod.

Z wersji roboczej C ++ 11 :

§ 6.6.3 / 2

Wypłynięcie z końca funkcji [...] powoduje niezdefiniowane zachowanie w funkcji zwracającej wartość.

§ 3.6.1 / 5

Jeśli kontrola osiągnie koniec mainbez napotkania return instrukcji, efektem jest wykonanie

return 0;

Zauważ, że zachowanie opisane w C ++ 6.6.3 / 2 nie jest takie samo w C.


gcc da ci ostrzeżenie, jeśli wywołasz go z opcją -Wreturn-type.

-Wreturn-type Ostrzegaj za każdym razem, gdy funkcja jest zdefiniowana z typem zwracanym, który domyślnie int. Ostrzegaj również o każdej instrukcji return bez wartości zwracanej w funkcji, której typ zwrotu nie jest void (wypadnięcie z końca ciała funkcji jest uważane za zwracające bez wartości) oraz o instrukcji return z wyrażeniem w funkcji, której return-type is void.

To ostrzeżenie jest włączane przez -Wall .


Jako ciekawostkę spójrz, co robi ten kod:

#include <iostream>

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

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

Ten kod ma formalnie niezdefiniowane zachowanie iw praktyce wywołuje zależne od konwencji i architektury . W jednym konkretnym systemie, z jednym konkretnym kompilatorem, wartość zwracana jest wynikiem oceny ostatniego wyrażenia, przechowywanej w eaxrejestrze procesora tego systemu.

fnieto - Fernando Nieto
źródło
13
Obawiałbym się nazywać niezdefiniowanych zachowań „dozwolonymi”, chociaż wprawdzie nie miałbym racji, nazywając je „zabronionymi”. Nie bycie błędem i niewymaganie diagnostyki to nie to samo, co „dozwolone”. Przynajmniej twoja odpowiedź brzmi trochę tak, jakbyś mówił, że można to zrobić, co w większości nie jest.
Wyścigi lekkości na orbicie,
4
@ Catskul, dlaczego kupujesz ten argument? Czy nie byłoby możliwe, jeśli nie banalnie łatwe, zidentyfikowanie punktów wyjścia funkcji i upewnienie się, że wszystkie zwracają wartość (i wartość zadeklarowanego typu zwracanego)?
BlueBomber
3
@ Catskul, tak i nie. Języki statycznie typowane i / lub kompilowane robią wiele rzeczy, które prawdopodobnie uznałbyś za „zbyt drogie”, ale ponieważ robią to tylko raz, w czasie kompilacji, ich koszt jest znikomy. Nawet powiedziawszy to, nie rozumiem, dlaczego identyfikowanie punktów wyjścia funkcji musi być superliniowe: po prostu przechodzisz przez AST funkcji i szukasz wywołań powrotu lub wyjścia. To czas liniowy, który jest zdecydowanie efektywny.
BlueBomber
3
@LightnessRacesinOrbit: Jeśli funkcja ze zwracaną wartością czasami zwraca natychmiastową wartość, a czasami wywołuje inną funkcję, która zawsze kończy działanie za pośrednictwem throwlub longjmp, czy kompilator powinien wymagać nieosiągalnego returnpo wywołaniu funkcji niezwracającej? Przypadki, w których nie jest to potrzebne, nie są zbyt częste, a wymóg jego uwzględnienia nawet w takich przypadkach prawdopodobnie nie byłby uciążliwy, ale decyzja, by tego nie wymagać, jest rozsądna.
supercat,
1
@supercat: superinteligentny kompilator nie ostrzeże ani nie wyświetli błędu w takim przypadku, ale - znowu - jest to w zasadzie nieobliczalne w przypadku ogólnym, więc utknąłeś z ogólną zasadą. Jeśli jednak wiesz, że twój koniec funkcji nigdy nie zostanie osiągnięty, to jesteś tak daleko od semantyki tradycyjnej obsługi funkcji, że tak, możesz iść dalej i zrobić to i wiedzieć, że jest to bezpieczne. Szczerze mówiąc, w tym momencie jesteś warstwą poniżej C ++ i jako taka, wszystkie jej gwarancje i tak są dyskusyjne.
Wyścigi lekkości na orbicie,
42

gcc domyślnie nie sprawdza, czy wszystkie ścieżki kodu zwracają wartość, ponieważ generalnie nie można tego zrobić. Zakłada, że ​​wiesz, co robisz. Rozważmy typowy przykład z wykorzystaniem wyliczeń:

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

    // Error, no return?
}

Ty programista wiesz, że poza błędem ta metoda zawsze zwraca kolor. gcc ufa, że ​​wiesz, co robisz, więc nie zmusza cię do umieszczania powrotu na dole funkcji.

Z drugiej strony javac próbuje zweryfikować, czy wszystkie ścieżki kodu zwracają wartość i zgłasza błąd, jeśli nie może udowodnić, że wszystkie to robią. Ten błąd jest wymagany przez specyfikację języka Java. Zauważ, że czasami jest to błędne i musisz wstawić niepotrzebną instrukcję powrotu.

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

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

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

To filozoficzna różnica. Języki C i C ++ są bardziej liberalne i ufają bardziej niż Java lub C #, dlatego niektóre błędy w nowszych językach są ostrzeżeniami w C / C ++, a niektóre ostrzeżenia są domyślnie ignorowane lub wyłączane.

John Kugelman
źródło
2
Gdyby javac faktycznie sprawdzał ścieżki kodu, czy nie zobaczyłby, że nigdy nie możesz osiągnąć tego punktu?
Chris Lutz
3
W pierwszym nie daje ci kredytu za pokrycie wszystkich przypadków wyliczenia (potrzebujesz domyślnego przypadku lub zwrotu po zmianie), aw drugim nie wie, że System.exit()nigdy nie wróci.
John Kugelman
2
Wydaje się proste, że javac (skądinąd potężny kompilator) wie, że System.exit()nigdy nie powróci. Sprawdziłem to ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ), a dokumentacja po prostu mówi, że „normalnie nigdy nie wraca”. Zastanawiam się, co to znaczy ...
Paul Biggar
@Paul: to znaczy, że nie mieli dobrego edytora. We wszystkich innych językach jest napisane „nigdy nie zwraca normalnie” - tj. „Nie zwraca przy użyciu normalnego mechanizmu zwrotu”.
Max Lybbert,
1
Zdecydowanie wolałbym kompilator, który przynajmniej ostrzegłby, gdyby napotkał pierwszy przykład, ponieważ poprawność logiki zepsuje się, jeśli ktoś doda nową wartość do wyliczenia. Chciałbym domyślną sprawę, która narzekała głośno i / lub uległa awarii (prawdopodobnie używając asercji).
Injektilo
14

Masz na myśli, dlaczego wypłynięcie z końca funkcji zwracającej wartość (tj. Wyjście bez jawności return) nie jest błędem?

Po pierwsze, w C to, czy funkcja zwraca coś znaczącego, czy nie, jest krytyczne tylko wtedy, gdy wykonujący kod faktycznie używa zwróconej wartości. Może język nie chciał zmusić Cię do zwrotu czegokolwiek, kiedy wiesz, że i tak nie będziesz go używać przez większość czasu.

Po drugie, najwyraźniej specyfikacja języka nie chciała zmusić autorów kompilatora do wykrycia i zweryfikowania wszystkich możliwych ścieżek kontrolnych pod kątem obecności jawnego return(chociaż w wielu przypadkach nie jest to takie trudne). Ponadto niektóre ścieżki kontrolne mogą prowadzić do funkcji niezwracających - cecha, która jest generalnie nieznana kompilatorowi. Takie ścieżki mogą stać się źródłem irytujących fałszywych alarmów.

Zauważ również, że C i C ++ różnią się definicjami zachowania w tym przypadku. W C ++ samo wypłynięcie z końca funkcji zwracającej wartość jest zawsze niezdefiniowanym zachowaniem (niezależnie od tego, czy wynik funkcji jest używany przez wywołujący kod). W C powoduje to niezdefiniowane zachowanie tylko wtedy, gdy kod wywołujący próbuje użyć zwróconej wartości.

Mrówka
źródło
+1, ale czy C ++ nie może pominąć returninstrukcji na końcu main()?
Chris Lutz
3
@Chris Lutz: Tak, mainjest pod tym względem wyjątkowy.
AnT
5

W C / C ++ legalne jest, aby nie powracać z funkcji, która twierdzi, że coś zwraca. Istnieje wiele przypadków użycia, takich jak wywołanie exit(-1)lub funkcja, która ją wywołuje lub zgłasza wyjątek.

Kompilator nie odrzuci legalnego C ++, nawet jeśli prowadzi to do UB, jeśli o to poprosisz. W szczególności prosisz o nie generowanie ostrzeżeń . (Gcc nadal domyślnie włącza niektóre, ale po dodaniu wydaje się, że są one zgodne z nowymi funkcjami, a nie nowymi ostrzeżeniami dla starych funkcji)

Zmiana domyślnego gcc bezargumentowego, aby emitował pewne ostrzeżenia, może być przełomową zmianą dla istniejących skryptów lub stworzyć systemy. Dobrze zaprojektowane -Walli radzą sobie z ostrzeżeniami lub przełączają poszczególne ostrzeżenia.

Nauka korzystania z łańcucha narzędzi C ++ jest barierą w nauce bycia programistą C ++, ale łańcuchy narzędzi C ++ są zazwyczaj pisane przez ekspertów i dla nich.

Yakk - Adam Nevraumont
źródło
Tak, w moim Makefilemam to uruchomione -Wall -Wpedantic -Werror, ale to był jednorazowy skrypt testowy, do którego zapomniałem podać argumenty.
Załóż pozew Moniki
2
Na przykład, tworzenie -Wduplicated-condczęści -Wallzepsutego bootstrapu GCC. Niektóre ostrzeżenia, które wydają się odpowiednie w większości kodu, nie są odpowiednie w całym kodzie. Dlatego nie są one domyślnie włączone.
och, ktoś potrzebuje lalek
Twoje pierwsze zdanie wydaje się być w sprzeczności z cytatem z zaakceptowanej odpowiedzi „Odpłynięcie… niezdefiniowane zachowanie…”. Czy ub jest uważany za „legalny”? A może masz na myśli to, że nie jest to UB, chyba że (nie) zwrócona wartość jest faktycznie używana? Martwię się przypadkiem C ++ przy okazji
idclev 463035818
@ tobi303 int foo() { exit(-1); }nie zwraca intfunkcji, która „twierdzi, że zwraca int”. Jest to legalne w C ++. Teraz nie zwraca niczego ; koniec tej funkcji nigdy nie zostanie osiągnięty. W rzeczywistości osiągnięcie końca foobyłoby niezdefiniowanym zachowaniem. Ignorowanie przypadków zakończenia procesu int foo() { throw 3.14; }również twierdzi, że zwraca, intale nigdy tego nie robi.
Yakk - Adam Nevraumont
1
więc myślę, że void* foo(void* arg) { pthread_exit(NULL); }jest w porządku z tego samego powodu (gdy jego jedyne użycie jest przez pthread_create(...,...,foo,...);)
idclev 463035818
1

Uważam, że jest to spowodowane starszym kodem (C nigdy nie wymagało instrukcji powrotu, podobnie jak C ++). Prawdopodobnie istnieje ogromna baza kodu zależna od tej „funkcji”. Ale przynajmniej jest -Werror=return-type flaga na wielu kompilatorach (w tym gcc i clang).

d.lozinski
źródło
1

W niektórych ograniczonych i rzadkich przypadkach może być przydatne wypłynięcie z końca funkcji, która nie jest pusta, bez zwracania wartości. Podobnie jak następujący kod specyficzny dla MSVC:

double pi()
{
    __asm fldpi
}

Ta funkcja zwraca liczbę pi przy użyciu zestawu x86. W przeciwieństwie do montażu w GCC, nie znam żadnego sposobu returnna zrobienie tego bez angażowania narzutu w wyniku.

O ile wiem, główne kompilatory C ++ powinny emitować przynajmniej ostrzeżenia dla pozornie nieprawidłowego kodu. Jeśli uczynię pi()treść pustą, GCC / Clang zgłosi ostrzeżenie, a MSVC zgłosi błąd.

Ludzie wspominali o wyjątkach i exitw niektórych odpowiedziach. To nie są ważne powody. Albo rzuca wyjątek, lub wywołanie exit, będzie nie sprawiają, że wykonanie funkcji odpływać do końca. Kompilatory to wiedzą: napisanie instrukcji throw lub wywołanie exitpustej treści polecenia pi()zatrzyma wszelkie ostrzeżenia lub błędy kompilatora.

Yongwei Wu
źródło
MSVC w szczególności obsługuje opadanie końca voidfunkcji niebędącej funkcją po tym, jak wbudowany asm pozostawia wartość w rejestrze wartości zwracanej. ( st0w tym przypadku x87 , EAX dla liczby całkowitej. I może xmm0 w konwencji wywoływania, która zwraca float / double w xmm0 zamiast st0). Definiowanie tego zachowania jest specyficzne dla MSVC; nawet nie zadzwonić -fasm-blocksdo obsługi tej samej składni sprawia, że ​​jest to bezpieczne. Zobacz czy __asm ​​{}; zwrócić wartość eax?
Peter Cordes
0

W jakich okolicznościach nie powoduje to błędu? Jeśli deklaruje typ zwracany i nic nie zwraca, brzmi to dla mnie jak błąd.

Jedynym wyjątkiem, o którym przychodzi mi do głowy, jest main()funkcja, która w ogóle nie potrzebuje returninstrukcji (przynajmniej w C ++; nie mam pod ręką żadnego ze standardów C). Jeśli nie ma powrotu, będzie działać tak, jakby return 0;była ostatnią instrukcją.

David Thornley
źródło
1
main()potrzebuje returnw C.
Chris Lutz
@Jefromi: OP pyta o nieważną funkcję bez return <value>;oświadczenia
Bill
2
main automatycznie zwraca 0 w C i C ++. C89 wymaga jawnego zwrotu.
Johannes Schaub - litb
1
@Chris: w C99 return 0;na końcu main()(i main()tylko) znajduje się niejawny . Ale i tak warto dodać styl return 0;.
pmg
0

Wygląda na to, że musisz podkręcić ostrzeżenia kompilatora:

$ 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
źródło
5
Powiedzenie „włącz -Werror” jest brakiem odpowiedzi. Oczywiście istnieje różnica w dotkliwości między problemami sklasyfikowanymi jako ostrzeżenia i błędy, a gcc traktuje ten problem jako mniej poważną klasę.
Cascabel
2
@Jefromi: Z czysto językowego punktu widzenia nie ma różnicy między ostrzeżeniami a błędami. Kompilator jest wymagany tylko do wydania „komunikatu disgnostycznego”. Nie ma wymogu zatrzymywania kompilacji lub nazywania czegoś „błędem”, a czegoś innego „ostrzeżeniem”. Po wysłaniu komunikatu diagnostycznego (lub jakiegokolwiek innego) decyzja należy wyłącznie do Ciebie.
AnT
1
Z drugiej strony, omawiany problem powoduje UB. Kompilatory nie muszą w ogóle łapać UB.
AnT
1
W 6.9.1 / 12 w n1256 jest napisane: "Jeśli}, który kończy funkcję, zostanie osiągnięty, a wartość wywołania funkcji jest używana przez wywołującego, zachowanie jest niezdefiniowane."
Johannes Schaub - litb
2
@Chris Lutz: Nie widzę tego. Użycie jawnego pustego return;elementu w funkcji nieważnej jest naruszeniem ograniczenia, a użycie return <value>;w funkcji nieważnej jest naruszeniem ograniczenia . Ale myślę, że to nie jest temat. OP, jak to zrozumiałem, polega na wyjściu z funkcji, która nie jest pusta bez znaku return(po prostu pozwala sterowaniu spływać z końca funkcji). To nie jest naruszenie ograniczenia, AFAIK. Standard mówi tylko, że zawsze jest to UB w C ++, a czasami UB w C.
AnT
-2

Jest to naruszenie ograniczenia w c99, ale nie w c89. Kontrast:

c89:

3.6.6.4 returnOświadczenie

Ograniczenia

returnOświadczenie o wyrażeniu nie powinny pojawiać się w funkcji, których typ jest powrót void.

c99:

6.8.6.4 returnOświadczenie

Ograniczenia

returnOświadczenie o wyrażeniu nie powinny pojawiać się w funkcji, których typ jest powrót void. returnOświadczenie bez wyrazu powinna pojawić się tylko w funkcji, których typ jest powrót void.

Nawet w --std=c99trybie gcc wyśle ​​tylko ostrzeżenie (chociaż bez potrzeby włączania dodatkowych -Wflag, jak jest to wymagane domyślnie lub w c89 / 90).

Edytuj, aby dodać, że w c89 „osiągnięcie tego, }co kończy funkcję, jest równoważne wykonaniu returninstrukcji bez wyrażenia” (3.6.6.4). Jednak w c99 zachowanie jest niezdefiniowane (6.9.1).

Matt B.
źródło
4
Zauważ, że dotyczy to tylko wyraźnych returnstwierdzeń. Nie obejmuje wypadnięcia końca funkcji bez zwrócenia wartości.
John Kugelman
3
Zauważ, że C99 pomija „osiągnięcie}, które kończy funkcję, jest równoznaczne z wykonaniem instrukcji return bez wyrażenia”, więc nie jest to naruszenie ograniczenia, a zatem nie jest wymagana diagnostyka.
Johannes Schaub - litb