Perché lo scorrimento al termine di una funzione non nulla senza la restituzione di un valore non produce un errore del compilatore?


158

Da quando ho capito molti anni fa che questo non produce un errore di default (almeno in GCC), mi sono sempre chiesto perché?

Comprendo che è possibile emettere flag di compilatore per generare un avviso, ma non dovrebbe sempre essere un errore? Perché ha senso che una funzione non nulla non restituisca un valore valido?

Un esempio come richiesto nei commenti:

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

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

... compila.


9
In alternativa, tratto tutti gli avvisi per quanto banali come errori, e attivo tutti gli avvisi che posso (con la disattivazione locale se necessario ... ma poi è chiaro nel codice perché).
Matthieu M.,

8
-Werror=return-typetratterà solo quell'avvertimento come un errore. Ho appena ignorato l'avvertimento e il paio di minuti di frustrazione rintracciare un invalidothis puntatore mi portano qui e fino a questa conclusione.
jozxyqk,

Ciò è aggravato dal fatto che scorre fuori dalla fine di un std::optional funzione senza tornare restituisce un "vero" optional
Rufus

@Rufus Non è necessario. Questo è proprio quello che è successo nel tuo ciclo macchina / compilatore / OS / lunare. Qualunque sia il codice spazzatura generato dal compilatore a causa del comportamento indefinito, è appena sembrato un "vero" opzionale, qualunque cosa sia.
underscore_d

Risposte:


146

Gli standard C99 e C ++ non richiedono funzioni per restituire un valore. La dichiarazione di ritorno mancante in una funzione di restituzione del valore verrà definita (per restituire 0) solo nella mainfunzione.

La logica include che verificare se ogni percorso di codice restituisce un valore è piuttosto difficile e un valore di ritorno può essere impostato con assemblatore incorporato o altri metodi complicati.

Dalla bozza C ++ 11 :

§ 6.6.3 / 2

L'uscita dalla fine di una funzione [...] provoca un comportamento indefinito in una funzione che restituisce valore.

§ 3.6.1 / 5

Se il controllo raggiunge la fine mainsenza incontrare return un'istruzione, l'effetto è quello dell'esecuzione

return 0;

Si noti che il comportamento descritto in C ++ 6.6.3 / 2 non è lo stesso in C.


gcc ti darà un avviso se lo chiami con l'opzione -Wreturn-type.

-Wreturn tipo Avvisa ogni volta che viene definita una funzione con un tipo di ritorno che per impostazione predefinita è int. Avvisa anche di qualsiasi istruzione return senza valore restituito in una funzione il cui tipo restituito non è nullo (la caduta dalla fine del corpo della funzione è considerata ritorno senza valore) e di un'istruzione return con un'espressione in una funzione il cui il tipo di ritorno è nullo.

Questo avviso è abilitato da -Wall .


Proprio come una curiosità, guarda cosa fa questo codice:

#include <iostream>

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

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

Questo codice ha un comportamento formalmente indefinito, e in pratica chiama convenzione e architettura dipendente. Su un particolare sistema, con un particolare compilatore, il valore restituito è il risultato della valutazione dell'ultima espressione, memorizzato nel eaxregistro del processore di quel sistema.


13
Diffiderei di chiamare il comportamento indefinito "permesso", anche se, a dire il vero, sbaglierei anche a chiamarlo "proibito". Non essere un errore e non richiedere una diagnostica non è esattamente lo stesso di "consentito". Per lo meno, la tua risposta dice un po 'come stai dicendo che va bene fare, che in gran parte non lo è.
Corse della leggerezza in orbita,

4
@Catskul, perché acquisti questo argomento? Non sarebbe possibile, se non banalmente facile, identificare i punti di uscita di una funzione e assicurarsi che tutti restituiscano un valore (e un valore del tipo restituito dichiarato)?
BlueBomber l'

3
@Catskul, sì e no. I linguaggi digitati staticamente e / o compilati fanno molte cose che probabilmente considereresti "proibitivamente costose", ma poiché lo fanno solo una volta, al momento della compilazione, hanno spese trascurabili. Anche detto questo, non vedo perché identificare i punti di uscita di una funzione debba essere super-lineare: attraversi semplicemente l'AST della funzione e cerchi le chiamate di ritorno o di uscita. Questo è tempo lineare, che è decisamente efficiente.
BlueBomber

3
@LightnessRacesinOrbit: se una funzione con un valore restituito a volte restituisce immediatamente un valore e talvolta chiama un'altra funzione che esce sempre tramite throwo longjmp, il compilatore dovrebbe richiedere un irraggiungibile returndopo la chiamata alla funzione non restituita? Il caso in cui non è necessario non è molto comune e un requisito per includerlo anche in tali casi probabilmente non sarebbe stato oneroso, ma la decisione di non richiederlo è ragionevole.
supercat

1
@supercat: un compilatore super-intelligente non avviserà o non si verificherà un errore in questo caso, ma - ancora una volta - questo è essenzialmente incalcolabile per il caso generale, quindi sei bloccato con una regola generale. Se sai, tuttavia, che la tua fine della funzione non sarà mai raggiunta, allora sei così lontano dalla semantica della gestione delle funzioni tradizionali che, sì, puoi andare avanti e fare questo e sapere che è sicuro. Francamente, sei a un livello inferiore al C ++ a quel punto e, come tale, tutte le sue garanzie sono comunque discutibili.
Razze di leggerezza in orbita,

42

gcc non verifica per impostazione predefinita che tutti i percorsi di codice restituiscano un valore perché in generale questo non può essere fatto. Si presume che tu sappia cosa stai facendo. Considera un esempio comune usando le enumerazioni:

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

    // Error, no return?
}

Il programmatore sa che, salvo un bug, questo metodo restituisce sempre un colore. gcc confida di sapere cosa stai facendo, quindi non ti costringe a mettere un ritorno in fondo alla funzione.

javac, d'altra parte, cerca di verificare che tutti i percorsi del codice restituiscano un valore e genera un errore se non può provare che tutti lo fanno. Questo errore è richiesto dalla specifica del linguaggio Java. Nota che a volte è sbagliato e devi inserire una dichiarazione di reso non necessaria.

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

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

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

È una differenza filosofica. C e C ++ sono linguaggi più permissivi e affidabili rispetto a Java o C # e quindi alcuni errori nelle lingue più recenti sono avvisi in C / C ++ e alcuni avvisi vengono ignorati o disattivati ​​per impostazione predefinita.


2
Se javac controlla effettivamente i percorsi dei codici non vedrebbe che non potresti mai raggiungere quel punto?
Chris Lutz,

3
Nel primo non ti dà credito per aver coperto tutti i casi enum (hai bisogno di un caso predefinito o di un reso dopo il passaggio), e nel secondo non sa che System.exit()non ritorna mai.
John Kugelman,

2
Sembra semplice per javac (un compilatore altrimenti potente) sapere che System.exit()non ritorna mai. L'ho cercato ( java.sun.com/j2se/1.4.2/docs/api/java/lang/… ) e i documenti dicono semplicemente che "non ritorna mai normalmente". Mi chiedo cosa significhi ...
Paul Biggar,

@Paul: significa che non avevano un buono. Tutte le altre lingue dicono "non ritorna mai normalmente", ovvero "non ritorna utilizzando il normale meccanismo di ritorno".
Max Lybbert,

1
Preferirei sicuramente un compilatore che avvertisse almeno se avesse incontrato quel primo esempio, perché la correttezza della logica si spezzerebbe se qualcuno aggiungesse un nuovo valore all'enum. Vorrei un caso predefinito che si lamentasse a gran voce e / o si schiantò (probabilmente usando un'asserzione).
Injektilo,

14

Intendi, perché returnnon è un errore sfuggire alla fine di una funzione di ritorno del valore (es. Uscire senza un esplicito )?

Innanzitutto, in C se una funzione restituisce qualcosa di significativo o meno è fondamentale solo quando il codice in esecuzione utilizza effettivamente il valore restituito. Forse la lingua non voleva costringerti a restituire nulla quando sai che non lo userai la maggior parte delle volte.

In secondo luogo, apparentemente le specifiche del linguaggio non volevano costringere gli autori del compilatore a rilevare e verificare tutti i possibili percorsi di controllo per la presenza di un esplicito return(sebbene in molti casi ciò non sia così difficile da fare). Inoltre, alcuni percorsi di controllo potrebbero portare a funzioni non di ritorno, il tratto generalmente sconosciuto al compilatore. Tali percorsi possono diventare una fonte di fastidiosi falsi positivi.

Si noti inoltre che in questo caso C e C ++ differiscono nelle loro definizioni del comportamento. In C ++, il semplice fatto di uscire dalla fine di una funzione di restituzione del valore è un comportamento indefinito (indipendentemente dal fatto che il risultato della funzione sia utilizzato dal codice chiamante). In C questo provoca un comportamento indefinito solo se il codice chiamante tenta di utilizzare il valore restituito.


+1 ma C ++ non può omettere le returndichiarazioni dalla fine di main()?
Chris Lutz,

3
@ Chris Lutz: Sì, mainè speciale al riguardo.
AnT

5

È legale in C / C ++ non tornare da una funzione che pretende di restituire qualcosa. Esistono numerosi casi d'uso, come la chiamata exit(-1), o una funzione che lo chiama o genera un'eccezione.

Il compilatore non rifiuterà C ++ legale anche se porta a UB se glielo stai chiedendo. In particolare, non si richiede la generazione di avvisi . (Gcc attiva ancora alcuni per impostazione predefinita, ma quando aggiunti quelli sembrano allinearsi con nuove funzionalità e non nuovi avvisi per le vecchie funzionalità)

La modifica del gcc no-arg predefinito per l'emissione di alcuni avvisi potrebbe essere una rottura sostanziale per gli script esistenti o creare sistemi. Anche quelli ben progettati-Wall e gestire gli avvisi o attivare gli avvisi individuali.

Imparare a usare una catena di strumenti C ++ è una barriera per imparare a essere un programmatore C ++, ma le catene di strumenti C ++ sono in genere scritte da e per esperti.


Sì, nel mio Makefilece l'ho a correre -Wall -Wpedantic -Werror, ma questo era uno script di test una tantum a cui mi ero dimenticato di fornire gli argomenti.
Finanzi la causa di Monica

2
Ad esempio, facendo -Wduplicated-condparte del -Wallbootstrap GCC rotto. Alcuni avvisi che sembrano appropriati nella maggior parte del codice non sono appropriati in tutto il codice. Ecco perché non sono abilitati per impostazione predefinita.
oh oh qualcuno ha bisogno di un burattinaio

la tua prima frase sembra essere in contraddizione con la citazione nella risposta accettata "Scorrendo .... comportamento indefinito ...". O ub è considerato "legale"? O vuoi dire che non è UB a meno che il valore (non) restituito sia effettivamente utilizzato? Sono preoccupato per il caso C ++ a proposito
idclev 463035818

@ tobi303 int foo() { exit(-1); }non ritorna intda una funzione che "afferma di restituire int". Questo è legale in C ++. Ora, non restituisce nulla ; la fine di quella funzione non è mai raggiunta. In realtà raggiungere la fine di foosarebbe un comportamento indefinito. Ignorando i casi di fine processo, int foo() { throw 3.14; }afferma anche di tornare intma non lo fa mai.
Yakk - Adam Nevraumont,

1
quindi immagino che void* foo(void* arg) { pthread_exit(NULL); }pthread_create(...,...,foo,...);
vada

1

Credo che ciò sia dovuto al codice legacy (C non ha mai richiesto la dichiarazione di ritorno così come C ++). Probabilmente esiste una base di codice enorme che si basa su quella "caratteristica". Ma almeno c'è -Werror=return-type flag su molti compilatori (inclusi gcc e clang).


1

In alcuni casi limitati e rari, può essere utile scorrere al termine di una funzione non nulla senza restituire un valore. Come il seguente codice specifico per MSVC:

double pi()
{
    __asm fldpi
}

Questa funzione restituisce pi usando l'assembly x86. A differenza dell'assemblaggio in GCC, non conosco alcun modo returnper farlo senza comportare spese generali nel risultato.

Per quanto ne so, i compilatori C ++ tradizionali dovrebbero emettere almeno avvisi per codice apparentemente non valido. Se faccio il corpo dipi() vuoto , GCC / Clang segnalerà un avviso e MSVC segnalerà un errore.

Le persone hanno menzionato eccezioni e exitin alcune risposte. Questi non sono motivi validi. Sia un'eccezione, o la chiamata exit, sarà non rendere l'esecuzione funzione defluire fine. E i compilatori lo sanno: scrivere un'istruzione di lancio o chiamare exitnel corpo vuoto pi()interromperà qualsiasi avvertimento o errore da un compilatore.


MSVC supporta specificamente la caduta dalla fine di una non voidfunzione dopo che asm inline ha lasciato un valore nel registro del valore restituito. (x87 st0in questo caso, EAX per intero. E forse xmm0 in una convenzione di chiamata che restituisce float / double in xmm0 anziché st0). La definizione di questo comportamento è specifica per MSVC; nemmeno un clangore -fasm-blocksper supportare la stessa sintassi lo rende sicuro. Vedi fa __asm ​​{}; restituire il valore di eax?
Peter Cordes,

0

In quali circostanze non produce un errore? Se dichiara un tipo restituito e non restituisce qualcosa, mi sembra un errore.

L'unica eccezione che mi viene in mente è la main()funzione, che non ha assolutamente bisogno di una returndichiarazione (almeno in C ++; non ho nessuno degli standard C a portata di mano). Se non c'è ritorno, agirà come se return 0;fosse l'ultima affermazione.


1
main()ha bisogno di un returnin C.
Chris Lutz,

@Jefromi: L'OP chiede una funzione non nulla senza una return <value>;dichiarazione
Bill

2
main restituisce automaticamente 0 in C e C ++. C89 ha bisogno di un ritorno esplicito.
Johannes Schaub - litb

1
@ Chris: in C99 c'è un implicito return 0;alla fine main()(e main()solo). Ma è comunque un buon stile da aggiungere return 0;.
pmg

0

Sembra che tu debba attivare gli avvisi del compilatore:

$ 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
Dire "attiva -Werror" è una non-risposta. Chiaramente c'è una differenza di gravità tra le questioni classificate come avvertenze ed errori, e gcc tratta questa come la classe meno severa.
Cascabel,

2
@Jefromi: dal punto di vista del linguaggio puro, non c'è differenza tra avvisi ed errori. Il compilatore deve solo emettere un "messaggio disgnostico". Non è necessario interrompere la compilazione o chiamare qualcosa di "un errore" e qualcos'altro "un avvertimento". Viene emesso un messaggio diagnostico (o di qualsiasi tipo), spetta a te decidere.
AnT

1
Inoltre, il problema in questione causa UB. I compilatori non sono tenuti a catturare affatto UB.
AnT

1
In 6.9.1 / 12 in n1256 dice "Se viene raggiunto il} che termina una funzione e il chiamante utilizza il valore della chiamata di funzione, il comportamento non è definito."
Johannes Schaub - litb

2
@ Chris Lutz: non lo vedo. È una violazione del vincolo utilizzare un vuoto esplicitoreturn; in una funzione non nulla ed è una violazione del vincolo utilizzare return <value>;in una funzione nulla. Ma questo, credo, non è l'argomento. L'OP, come ho capito, riguarda l'uscita da una funzione non nulla senza un return(solo consentendo al controllo di fluire al termine della funzione). Non è una violazione del vincolo, AFAIK. Lo standard dice solo che è sempre UB in C ++ e talvolta UB in C.
AnT

-2

È una violazione del vincolo in c99, ma non in c89. Contrasto:

C89:

3.6.6.4 return dichiarazione

vincoli

Una returndichiarazione con un'espressione non deve apparire in una funzione il cui tipo restituito è void.

C99:

6.8.6.4 La returndichiarazione

vincoli

Una returndichiarazione con un'espressione non deve apparire in una funzione il cui tipo restituito è void. Una returndichiarazione senza espressione deve apparire solo in una funzione il cui tipo restituito è void.

Anche in --std=c99modalità, gcc emetterà solo un avviso (anche se senza la necessità di abilitare -Wflag aggiuntivi , come richiesto di default o in c89 / 90).

Modifica per aggiungere che in c89, "raggiungere il }termine che termina una funzione equivale a eseguire returnun'istruzione senza un'espressione" (3.6.6.4). Tuttavia, in c99 il comportamento non è definito (6.9.1).


4
Si noti che questo copre solo returndichiarazioni esplicite . Non copre la caduta dalla fine di una funzione senza restituire un valore.
John Kugelman,

3
Si noti che C99 manca "raggiungere il} che termina una funzione equivale a eseguire una dichiarazione di ritorno senza un'espressione", quindi non ha fatto una violazione di vincolo e quindi non è richiesta alcuna diagnostica.
Johannes Schaub - litb
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.