Perché compila questo snippet C ++ (la funzione non void non restituisce un valore)

L’ho trovato in una delle mie biblioteche questa mattina:

static tvec4 Min(const tvec4& a, const tvec4& b, tvec4& out) { tvec3::Min(a,b,out); out.w = min(aw,bw); } 

Mi aspetto un errore del compilatore perché questo metodo non restituisce nulla e il tipo restituito non è void .

Le uniche due cose che vengono in mente sono

Non ho trovato la parte delle specifiche C ++ che risolve questo problema. I riferimenti (ha) sono apprezzati.

Aggiornare

In alcune circostanze, ciò genera un errore in VS2012. Non ho ristretto le specifiche, ma è comunque interessante.

Questo è un comportamento non definito dalla bozza standard C ++ 11 sezione 6.6.3 L’istruzione return paragrafo 2 che dice:

[…] Scorrere alla fine di una funzione equivale a un ritorno senza valore; questo si traduce in un comportamento indefinito in una funzione di ritorno del valore. […]

Ciò significa che il compilatore non è obbligato a fornire un errore o un avviso di solito perché può essere difficile da diagnosticare in tutti i casi. Possiamo vedere questo dalla definizione di comportamento non definito nella bozza standard nella sezione 1.3.24 che dice:

[…] Il comportamento indefinito ammissibile va dall’ignorare completamente la situazione con risultati imprevedibili, a comportarsi durante la traduzione o l’esecuzione del programma in un modo documentato caratteristico dell’ambiente (con o senza emissione di un messaggio diagnostico), a terminare una traduzione o esecuzione (con l’emissione di un messaggio diagnostico). […]

Anche se in questo caso possiamo ottenere sia gcc che clang per generare un wanring usando il flag -Wall , che mi dà un avvertimento simile a questo:

avviso: il controllo raggiunge la fine della funzione non vuoto [-Wreturn-type]

Possiamo trasformare questo avviso particolare in un errore usando il -Werror=return-type . Mi piace anche usare -Wextra -Wconversion -pedantic per i miei progetti personali.

Come ComicSansMS menziona in Visual Studio questo codice genererebbe C4716 che è un errore di default, il messaggio che vedo è:

errore C4716: ‘Min’: deve restituire un valore

e nel caso in cui non tutti i percorsi di codice restituiscano un valore, allora genererebbe C4715 , che è un avvertimento.

Forse qualche elaborazione sul perché parte della domanda:

Come risulta, in realtà è piuttosto difficile † per un compilatore C ++ per determinare se una funzione termina senza un valore di ritorno. Oltre ai percorsi di codice che terminano con dichiarazioni di ritorno esplicite e quelli che cadono dalla fine della funzione, devi considerare anche i potenziali lanci di eccezioni o longjmp s nella funzione stessa, così come tutti i suoi calle.

Mentre è abbastanza facile per un compilatore identificare una funzione che sembra che manchi un ritorno, è molto più difficile dimostrare che manca un ritorno. Per sollevare i produttori di compilatori da questo onere, lo standard non richiede questo per generare un errore.

Quindi i produttori di compilatori sono liberi di generare un avviso se sono abbastanza sicuri che una funzione non ha un ritorno e l’utente è quindi libero di ignorare / mascherare quell’avvertimento in quei rari casi in cui il compilatore era in realtà sbagliato.

†: Nel caso generale, questo è equivalente al problema dell’arresto , quindi è effettivamente imansible per una macchina decidere in modo affidabile.

Compila il tuo codice con l’opzione -Wreturn-type :

 $ g++ -Wreturn-type source.cpp 

Questo ti darà un avvertimento . È ansible trasformare l’avviso in errore se si utilizza anche -Werror :

 $ g++ -Wreturn-type -Werror source.cpp 

Notare che ciò trasformsrà tutti gli avvisi in errori. Quindi, se vuoi un errore per un avvertimento specifico, ad esempio -Wreturn-type , digita semplicemente return-type senza -W parte come:

 $ g++ -Werror=return-type source.cpp 

In generale, dovresti sempre usare -Wall opzione -Wall che include gli avvertimenti più comuni – questo include anche la dichiarazione di ritorno mancante. Insieme a -Wall , puoi usare anche -Wextra , che include altri avvisi non inclusi da -Wall .

Forse qualche ulteriore elaborazione sul perché parte della domanda.

C ++ è stato progettato in modo tale che un corpo molto grande di codice preesistente di codice C compili con una quantità minima di modifiche. Sfortunatamente, la stessa C stava pagando un dovere simile al C pre-standard che non aveva nemmeno la parola chiave void e invece faceva affidamento su un tipo di ritorno predefinito di int . Le funzioni C di solito restituivano valori, e ogni volta che veniva scritto codice in modo superficiale simile alle procedure Algol / Pascal / Basic senza dichiarazioni di return , la funzione era, sotto la cappa, di restituire qualsiasi cosa fosse lasciata in pila. Né il chiamante né il callee assegnano il valore della spazzatura in modo affidabile. Se la spazzatura viene quindi ignorata da ogni chiamante, tutto va bene e C ++ eredita l’obbligo morale di compilare tale codice.

(Se il valore restituito viene utilizzato dal chiamante, il codice può comportarsi in modo non deterministico, simile all’elaborazione di una variabile non inizializzata. La differenza può essere identificata attendibilmente da un compilatore, in un ipotetico linguaggio successore in C? Il chiamante e il chiamato possono trovarsi in diverse unità di compilazione.)

L’ int implicito è solo una parte dell’eredità C qui coinvolta. Una funzione “dispatcher” potrebbe, a seconda di un parametro, restituire una varietà di tipi da alcune branch di codice e non restituire alcun valore utile da altre branch di codice. In genere, una funzione di questo tipo viene dichiarata per restituire un tipo abbastanza lungo da contenere uno qualsiasi dei tipi possibili e il chiamante potrebbe doverlo lanciare o estrarlo da un union .

Quindi la causa più profonda è probabilmente la convinzione dei creatori del linguaggio C che le procedure che non restituiscono alcun valore sono solo un caso speciale non importante di funzioni che lo fanno; questo problema è stato aggravato dalla mancanza di concentrazione sul tipo di sicurezza delle chiamate di funzione nei dialetti C più vecchi.

Mentre C ++ ha rotto la compatibilità con alcuni degli aspetti peggiori di C ( esempio ), la disponibilità a compilare un’istruzione return senza un valore (o il ritorno implicito senza valore alla fine di una funzione) non era uno di questi.

Come già accennato, questo è un comportamento indefinito e ti darà un avvertimento sul compilatore. La maggior parte dei luoghi in cui ho lavorato richiedono di triggersre le impostazioni del compilatore per trattare gli avvertimenti come errori – il che impone che tutto il codice debba essere compilato con 0 errori e 0 avvisi. Questo è un buon esempio del perché è una buona idea.

Questa è più della regola / funzione C ++ standard che tende ad essere flessibile con le cose e che tende ad essere più vicina a C.

Ma quando parliamo di compilatori, GCC o VS, sono più utili per l’uso professionale e per vari scopi di sviluppo e quindi introducono regole di sviluppo più rigide secondo le vostre esigenze.

Questo ha anche senso, secondo la mia opinione personale, perché la lingua riguarda le funzionalità e il loro utilizzo, mentre il compilatore definisce le regole per il modo ottimale e migliore di usarlo secondo le proprie esigenze.

Come accennato nel post precedente, il compilatore a volte dà l’errore, a volte dà un avvertimento e ha anche la possibilità di saltare questi avvertimenti, ecc, indicando la libertà di usare il linguaggio e le sue caratteristiche in un modo che ci si adatta meglio.

Oltre a questo ci sono molte altre domande che menzionano questo comportamento nel restituire un risultato senza avere una dichiarazione di return . Un semplice esempio potrebbe essere:

 int foo(int a, int b){ int c = a+b;} int main(){ int c = 5; int d = 5; printf("f(%d,%d) is %d\n", c, d, foo(c,d)); return 0; } 

Questa anomalia potrebbe essere dovuta alle proprietà dello stack e più precisamente:

Macchine a indirizzo zero

Nelle macchine con indirizzo zero, si presuppone che le posizioni di entrambi gli operandi si trovino in una posizione predefinita . Queste macchine usano lo stack come sorgente degli operandi di input e il risultato torna nello stack. Stack è una struttura di dati LIFO (last-in-first-out) supportata da tutti i processori, indipendentemente dal fatto che si tratti di macchine con zero indirizzi. Come suggerisce il nome, l’ultimo object messo in pila è il primo object che deve essere estratto dalla pila. Tutte le operazioni su questo tipo di macchina presuppongono che gli operandi input richiesti siano i primi due valori nello stack. Il risultato dell’operazione viene posizionato in cima allo stack.

In aggiunta a ciò, per accedere alla memoria per leggere e scrivere i dati stessi registri vengono utilizzati come origine dati e destinazione (registro DS (segmento dati)), che memorizzano prima le variabili necessarie per il calcolo e quindi il risultato restituito.

Nota:

con questa risposta vorrei discutere una ansible spiegazione dello strano comportamento a livello di macchina (istruzione) poiché ha già un contesto e è coperto in una gamma sufficientemente ampia.