Inizializza sempre le tue variabili
La differenza tra le situazioni che stai prendendo in considerazione è che il caso senza inizializzazione comporta un comportamento indefinito , mentre il caso in cui hai impiegato del tempo per inizializzare crea un bug ben definito e deterministico . Non posso sottolineare quanto siano abbastanza diversi questi due casi.
Considera un esempio ipotetico che potrebbe essere accaduto a un ipotetico dipendente in un ipotetico programma di simulazioni. Questo ipotetico team stava ipoteticamente cercando di fare una simulazione deterministica per dimostrare che il prodotto che stavano ipoteticamente vendendo rispondeva alle esigenze.
Va bene, mi fermo con la parola iniezioni. Penso che tu abbia capito il punto ;-)
In questa simulazione, c'erano centinaia di variabili non inizializzate. Uno sviluppatore ha eseguito valgrind sulla simulazione e ha notato che c'erano diversi errori "ramo su valore non inizializzato". "Hmm, sembra che ciò potrebbe causare non determinismo, rendendo difficile ripetere le prove quando ne abbiamo più bisogno." Lo sviluppatore è andato alla direzione, ma la direzione era molto stretta e non poteva risparmiare risorse per rintracciare questo problema. "Finiamo per inizializzare tutte le nostre variabili prima di usarle. Abbiamo buone pratiche di codifica."
Pochi mesi prima della consegna finale, quando la simulazione è in piena modalità di abbandono, e l'intero team sta scattando per completare tutte le cose che la gestione ha promesso con un budget che, come ogni progetto mai finanziato, era troppo piccolo. Qualcuno ha notato che non potevano testare una caratteristica essenziale perché, per qualche ragione, la sim deterministica non si stava comportando in modo deterministico per il debug.
L'intero team potrebbe essere stato fermato e aver trascorso la parte migliore di 2 mesi a pettinare l'intera base di codice di simulazione correggendo errori di valore non inizializzati invece di implementare e testare funzionalità. Inutile dire che il dipendente ha saltato il "Te l'avevo detto" ed è andato subito ad aiutare gli altri sviluppatori a capire quali sono i valori non inizializzati. Stranamente, gli standard di codifica sono stati cambiati poco dopo questo incidente, incoraggiando gli sviluppatori a inizializzare sempre le loro variabili.
E questo è il colpo di avvertimento. Questo è il proiettile che ti ha sfiorato il naso. Il vero problema è di gran lunga molto più insidioso di quanto tu possa immaginare.
L'uso di un valore non inizializzato è un "comportamento indefinito" (ad eccezione di alcuni casi angolari come char
). Un comportamento indefinito (o UB in breve) è così follemente e completamente dannoso per te, che non dovresti mai mai credere che sia migliore dell'alternativa. A volte puoi identificare che il tuo particolare compilatore definisce l'UB, e quindi è sicuro da usare, ma per il resto, il comportamento indefinito è "qualsiasi comportamento che il compilatore prova". Potrebbe fare qualcosa che definiresti "sano di mente" come se avesse un valore non specificato. Potrebbe emettere codici operativi non validi, causando potenzialmente la corruzione del programma. Potrebbe attivare un avviso al momento della compilazione oppure il compilatore potrebbe addirittura considerarlo un errore.
O potrebbe non fare nulla
Il mio canarino nella miniera di carbone per UB è un caso di un motore SQL di cui ho letto. Perdonami per non averlo collegato, non sono riuscito a trovare di nuovo l'articolo. Si è verificato un problema di sovraccarico del buffer nel motore SQL quando si è passato una dimensione del buffer più grande a una funzione, ma solo su una versione particolare di Debian. Il bug è stato debitamente registrato ed esplorato. La parte divertente è stata: il sovraccarico del buffer è stato verificato . C'era del codice per gestire il sovraccarico del buffer in posizione. Sembrava qualcosa del genere:
// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
// If dataLength is very large, we might overflow the pointer
// arithmetic, and end up with some very small pointer number,
// causing us to fail to realize we were trying to write past the
// end. Check this before we continue
if (put + dataLength < put)
{
RaiseError("Buffer overflow risk detected");
return 0;
}
...
// typical ring-buffer pointer manipulation followed...
}
Ho aggiunto altri commenti nella mia interpretazione, ma l'idea è la stessa. Se si put + dataLength
avvolge, sarà più piccolo del put
puntatore (avevano controlli del tempo di compilazione per assicurarsi che int senza segno avesse le dimensioni di un puntatore, per i curiosi). Se ciò accade, sappiamo che gli algoritmi standard del buffer ad anello potrebbero essere confusi da questo overflow, quindi restituiamo 0. O no?
A quanto pare, l'overflow sui puntatori non è definito in C ++. Poiché la maggior parte dei compilatori sta trattando i puntatori come numeri interi, finiamo con comportamenti tipici di overflow di numeri interi, che risultano essere il comportamento che desideriamo. Tuttavia, si tratta di un comportamento indefinito, il che significa che al compilatore è consentito fare tutto ciò che desidera.
Nel caso di questo bug, Debian è accaduto a scegliere di utilizzare una nuova versione di gcc che nessuna delle altre principali distribuzioni Linux era aggiornato al loro uscite di produzione. Questa nuova versione di gcc aveva un ottimizzatore del codice morto più aggressivo. Il compilatore ha visto il comportamento indefinito e ha deciso che il risultato if
dell'istruzione sarebbe "qualunque cosa renda meglio l'ottimizzazione del codice", che era una traduzione assolutamente legale di UB. Di conseguenza, ha assunto il presupposto che poiché ptr+dataLength
non può mai essere inferiore ptr
senza un overflow del puntatore UB, l' if
istruzione non si innescherebbe mai e ha ottimizzato il controllo del sovraccarico del buffer.
L'uso di UB "sano" in realtà ha fatto sì che un importante prodotto SQL avesse un exploit sovraccarico di buffer che aveva scritto codice per evitare!
Non fare affidamento su comportamenti indefiniti. Mai.
bytes_read
non viene modificato (quindi mantenuto zero), perché dovrebbe essere un bug? Il programma potrebbe continuare in modo sano purché non preveda implicitamente inbytes_read!=0
seguito. Quindi va bene i disinfettanti non si lamentano. D'altra parte, quandobytes_read
non è stato inizializzato in anticipo, il programma non sarà in grado di continuare in modo sano, quindi la mancata inizializzazione inbytes_read
realtà introduce un bug che prima non c'era.