Perché è pericoloso?
goto
non causa instabilità da solo. Nonostante circa 100.000 goto
s, il kernel Linux è ancora un modello di stabilità.
goto
da solo non dovrebbe causare vulnerabilità di sicurezza. In alcune lingue, tuttavia, la combinazione con i blocchi di gestione try
/ delle catch
eccezioni potrebbe comportare vulnerabilità, come spiegato nella presente raccomandazione CERT . I compilatori C ++ mainstream contrassegnano e impediscono tali errori, ma sfortunatamente i compilatori più vecchi o più esotici no.
goto
causa codice illeggibile e non mantenibile. Questo è anche chiamato codice spaghetti , perché, come in un piatto di spaghetti, è molto difficile seguire il flusso di controllo quando ci sono troppe goto.
Anche se riesci a evitare il codice spaghetti e se usi solo alcune goto, facilitano comunque bug come e perdite di risorse:
- Il codice che utilizza la programmazione della struttura, con blocchi nidificati chiari e loop o switch, è facile da seguire; il suo flusso di controllo è molto prevedibile. È quindi più facile garantire il rispetto degli invarianti.
- Con una
goto
dichiarazione, interrompi quel flusso diretto e rompi le aspettative. Ad esempio, potresti non notare che devi ancora liberare risorse.
- Molti
goto
in luoghi diversi possono inviarti a un singolo target goto. Quindi non è ovvio sapere con certezza lo stato in cui ti trovi quando raggiungi questo posto. Il rischio di formulare ipotesi errate / infondate è quindi piuttosto grande.
Ulteriori informazioni e preventivi:
C fornisce l' goto
istruzione infinitamente abusabile e le etichette a cui ramificare. Formalmente goto
non è mai necessario, e in pratica è quasi sempre facile scrivere codice senza di esso. (...)
Tuttavia, suggeriremo alcune situazioni in cui i goto possono trovare un posto. L'uso più comune è quello di abbandonare l'elaborazione in alcune strutture profondamente annidate, come la rottura di due loop contemporaneamente. (...)
Anche se non siamo dogmatici sulla questione, sembra che le dichiarazioni goto debbano essere usate con parsimonia, se non del tutto .
Quando si può usare Goto?
Come K&R, non sono dogmatico di goto. Ammetto che ci sono situazioni in cui goto potrebbe essere di facilitare la propria vita.
In genere, in C, goto consente l'uscita del loop multilivello o la gestione degli errori che richiede di raggiungere un punto di uscita appropriato che libera / sblocca tutte le risorse che sono state allocate finora (allocazione multipla in sequenza significa più etichette). Questo articolo quantifica i diversi usi di goto nel kernel di Linux.
Personalmente preferisco evitarlo e in 10 anni di C ho usato massimo 10 goto. Preferisco usare if
s nidificati , che penso siano più leggibili. Quando ciò porterebbe a un annidamento troppo profondo, sceglierei di decomporre la mia funzione in parti più piccole o di utilizzare un indicatore booleano in cascata. I compilatori di ottimizzazione di oggi sono abbastanza intelligenti da generare quasi lo stesso codice dello stesso codice con goto
.
L'uso di goto dipende fortemente dalla lingua:
In C ++, l'uso corretto di RAII fa sì che il compilatore distrugga automaticamente gli oggetti che escono dall'ambito, in modo che le risorse / il blocco vengano comunque ripuliti e che non sia più necessario andare a goto.
In Java non c'è alcun bisogno di goto (vedi citazione autore di Java sopra e questa eccellente risposta Stack Overflow ): il garbage collector che pulisce il disordine, break
, continue
, e try
/ catch
gestione delle eccezioni coprire tutto il caso in cui goto
potrebbe essere utile, ma in un più sicuro e migliore maniera. La popolarità di Java dimostra che la dichiarazione goto può essere evitata in un linguaggio moderno.
Ingrandisci la famosa vulnerabilità SSL goto fail
Importante dichiarazione di non responsabilità: in considerazione della feroce discussione nei commenti, voglio chiarire che non pretendo che la dichiarazione goto sia l'unica causa di questo errore. Non pretendo che senza goto non ci sarebbe nessun bug. Voglio solo mostrare che un goto può essere coinvolto in un bug grave.
Non so a quanti gravi bug siano collegati goto
nella storia della programmazione: i dettagli spesso non vengono comunicati. Tuttavia, c'era un famoso bug Apple SSL che indeboliva la sicurezza di iOS. L'affermazione che ha portato a questo errore è stata goto
un'affermazione errata .
Alcuni sostengono che la causa principale del bug non sia stata l'istruzione goto in sé, ma una copia / incolla errata, un rientro fuorviante, la mancanza di parentesi graffe attorno al blocco condizionale o forse le abitudini di lavoro dello sviluppatore. Non posso né confermarne nessuno: tutti questi argomenti sono ipotesi e interpretazioni probabili. Nessuno lo sa davvero. ( nel frattempo, l'ipotesi di una fusione che è andata storta come qualcuno ha suggerito nei commenti sembra essere un ottimo candidato in vista di altre incongruenze di rientro nella stessa funzione ).
L'unico fatto oggettivo è che un duplicato ha goto
portato all'uscita prematura della funzione. Guardando il codice, l'unica altra singola istruzione che avrebbe potuto causare lo stesso effetto sarebbe stata un ritorno.
L'errore è in funzione SSLEncodeSignedServerKeyExchange()
in questo file :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
In effetti le parentesi graffe attorno al blocco condizionale avrebbero potuto prevenire l'errore:
avrebbe portato a un errore di sintassi durante la compilazione (e quindi a una correzione) o a un goto innocuo ridondante. A proposito, GCC 6 sarebbe in grado di individuare questi errori grazie al suo avviso opzionale per rilevare rientri incoerenti.
Ma in primo luogo, tutte queste goto avrebbero potuto essere evitate con un codice più strutturato. Quindi goto è almeno indirettamente una causa di questo errore. Esistono almeno due modi diversi che avrebbero potuto evitarlo:
Approccio 1: se clausola o nidificati if
s
Invece di testare molte condizioni per errori in sequenza, e ogni volta che si invia a fail
un'etichetta in caso di problemi, si potrebbe optare per l'esecuzione delle operazioni crittografiche in una if
dichiarazione che lo farebbe solo se non ci fosse una pre-condizione errata:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Approccio 2: utilizzare un accumulatore di errori
Questo approccio si basa sul fatto che quasi tutte le istruzioni qui richiamano una funzione per impostare un err
codice di errore ed eseguono il resto del codice solo se err
era 0 (ovvero, funzione eseguita senza errore). Una buona alternativa sicura e leggibile è:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Qui, non c'è un singolo goto: nessun rischio di saltare rapidamente al punto di uscita del guasto. E visivamente sarebbe facile individuare una linea disallineata o dimenticata ok &&
.
Questo costrutto è più compatto. Si basa sul fatto che in C, la seconda parte di un logico e ( &&
) viene valutata solo se la prima parte è vera. In effetti, l'assemblatore generato da un compilatore ottimizzatore è quasi equivalente al codice originale con gotos: l'ottimizzatore rileva molto bene la catena di condizioni e genera codice, che al primo valore di ritorno non nullo salta alla fine ( prova online ).
Si potrebbe anche prevedere un controllo di coerenza al termine della funzione che durante la fase di test potrebbe identificare discrepanze tra il flag ok e il codice di errore.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Errori di questo tipo ==0
sostituiti inavvertitamente con errori di un !=0
connettore logico o sarebbero facilmente individuati durante la fase di debug.
Come detto: non pretendo che costrutti alternativi avrebbero evitato qualsiasi errore. Voglio solo dire che avrebbero potuto rendere più difficile il verificarsi del bug.