"Inizializzare sempre le variabili" non porta a nascondere bug importanti?


35

Le Linee guida di base C ++ hanno la regola ES.20: Inizializza sempre un oggetto .

Evita errori usati prima del set e il loro comportamento indefinito associato. Evita i problemi con la comprensione di un'inizializzazione complessa. Semplifica il refactoring.

Ma questa regola non aiuta a trovare i bug, li nasconde solo.
Supponiamo che un programma abbia un percorso di esecuzione in cui utilizza una variabile non inizializzata. È un bug. Comportamento indefinito a parte, significa anche che qualcosa è andato storto e il programma probabilmente non soddisfa i requisiti del prodotto. Quando sarà distribuito alla produzione, ci può essere una perdita di denaro, o anche peggio.

Come selezioniamo i bug? Scriviamo test. Ma i test non coprono il 100% dei percorsi di esecuzione e i test non coprono mai il 100% degli input del programma. Inoltre, anche un test copre un percorso di esecuzione errato: può ancora passare. Dopotutto è un comportamento indefinito, una variabile non inizializzata può avere un valore alquanto valido.

Ma oltre ai nostri test, abbiamo i compilatori che possono scrivere qualcosa come 0xCDCDCDCD su variabili non inizializzate. Ciò migliora leggermente il tasso di rilevamento dei test.
Ancora meglio - ci sono strumenti come Address Sanitizer, che catturerà tutte le letture di byte di memoria non inizializzati.

E infine ci sono analizzatori statici, che possono guardare il programma e dire che c'è un read-before-set su quel percorso di esecuzione.

Quindi abbiamo molti strumenti potenti, ma se inizializziamo la variabile - i disinfettanti non trovano nulla .

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

C'è un'altra regola: se l'esecuzione del programma rileva un bug, il programma dovrebbe morire il più presto possibile. Non c'è bisogno di tenerlo in vita, basta schiantarsi, scrivere un crashdump, consegnarlo agli ingegneri per le indagini.
L'inizializzazione delle variabili inutilmente fa il contrario: il programma viene mantenuto in vita, quando altrimenti avrebbe già un errore di segmentazione.


10
Anche se penso che questa sia una buona domanda, non capisco il tuo esempio. Se si verifica un errore di lettura e bytes_readnon viene modificato (quindi mantenuto zero), perché dovrebbe essere un bug? Il programma potrebbe continuare in modo sano purché non preveda implicitamente in bytes_read!=0seguito. Quindi va bene i disinfettanti non si lamentano. D'altra parte, quando bytes_readnon è stato inizializzato in anticipo, il programma non sarà in grado di continuare in modo sano, quindi la mancata inizializzazione in bytes_readrealtà introduce un bug che prima non c'era.
Doc Brown, il

2
@Abyx: anche se è di terze parti, se non si occupa di un buffer a partire da \0esso è difettoso. Se è stato documentato di non gestirlo, il tuo codice chiamante è difettoso. Se correggi il codice chiamante per verificarlo bytes_read==0prima dell'uso, tornerai al punto di partenza: il tuo codice è difettoso se non lo inizializzi bytes_read, sicuro se lo fai. ( Di solito le funzioni dovrebbero riempire i loro parametri di uscita anche in caso di errore : non proprio. Molto spesso le uscite sono lasciate sole o indefinite.)
Mat

1
C'è qualche motivo per cui questo codice ignora il err_treso da my_read()? Se c'è un bug in qualsiasi punto dell'esempio, questo è tutto.
Blrfl,

1
È facile: inizializza le variabili solo se è significativo. Se non lo è, non farlo. Posso essere d'accordo sul fatto che l'uso di dati "fittizi" per farlo è un male, perché nasconde i bug.
Pieter B,

1
"C'è un'altra regola: se l'esecuzione del programma incontra un bug, il programma dovrebbe morire il più presto possibile. Non c'è bisogno di tenerlo in vita, basta un crash, scrivere un crashdump, darlo agli ingegneri per le indagini.": Provalo su un volo software di controllo. Buona fortuna per recuperare la discarica dal disastro dell'aereo.
Giorgio,

Risposte:


44

Il tuo ragionamento va storto su diversi account:

  1. Gli errori di segmentazione sono tutt'altro che certi. L'uso di una variabile non inizializzata comporta un comportamento indefinito . Gli errori di segmentazione sono uno dei modi in cui tale comportamento può manifestarsi, ma apparire come normale è altrettanto probabile.
  2. I compilatori non riempiono mai la memoria non inizializzata con un modello definito (come 0xCD). Questo è qualcosa che alcuni debugger fanno per aiutarti a trovare luoghi in cui vengono utilizzate le variabili non inizializzate. Se si esegue un programma del genere al di fuori di un debugger, la variabile conterrà immondizia completamente casuale. È altrettanto probabile che un contatore come il bytes_readabbia il valore 10in quanto ha il valore 0xcdcdcdcd.
  3. Anche se si esegue un debugger che imposta la memoria non inizializzata su un modello fisso, lo fanno solo all'avvio. Ciò significa che questo meccanismo funziona in modo affidabile solo per variabili statiche (e possibilmente allocate in heap). Per le variabili automatiche, che vengono allocate nello stack o che vivono solo in un registro, è molto probabile che la variabile sia memorizzata in una posizione utilizzata in precedenza, quindi il modello di memoria rivelatore è già stato sovrascritto.

L'idea alla base della guida per inizializzare sempre le variabili è di abilitare queste due situazioni

  1. La variabile contiene un valore utile fin dall'inizio della sua esistenza. Se lo combini con la guida per dichiarare una variabile solo quando ne hai bisogno, puoi evitare che i futuri programmatori di manutenzione cadano nella trappola di iniziare a usare una variabile tra la sua dichiarazione e il primo incarico, dove la variabile esisterebbe ma non sarà inizializzata.

  2. La variabile contiene un valore definito che è possibile verificare in un secondo momento, per dire se una funzione come my_readha aggiornato il valore. Senza l'inizializzazione, non è possibile sapere se bytes_readha effettivamente un valore valido, perché non è possibile sapere con quale valore è iniziato.


8
1) si tratta di probabilità, come l'1% contro il 99%. 2 e 3) VC ++ genera tale codice di inizializzazione, anche per le variabili locali. 3) Le variabili statiche (globali) vengono sempre inizializzate con 0.
Abyx,

5
@Abyx: 1) Nella mia esperienza, la probabilità è ~ 80% "nessuna differenza comportamentale immediatamente evidente", 10% "fa la cosa sbagliata", 10% "segfault". Per quanto riguarda (2) e (3): VC ++ lo fa solo nelle build di debug. Affidarsi a questa è una pessima idea poiché interrompe selettivamente le build di rilascio e non si presenta in molti dei tuoi test.
Christian Aichinger,

8
Penso che "l'idea alla base della guida" sia la parte più importante di questa risposta. La guida non ti dice assolutamente di seguire ogni dichiarazione di variabile con = 0;. L'intento del consiglio è dichiarare la variabile nel punto in cui si avrà un valore utile per esso e assegnare immediatamente questo valore. Ciò è esplicitamente chiarito nelle regole immediatamente seguenti ES21 ed ES22. Quei tre dovrebbero essere tutti intesi come lavorare insieme; non come regole individuali non correlate.
GrandOpener,

1
@GrandOpener Exactly. Se non esiste alcun valore significativo da assegnare nel punto in cui viene dichiarata la variabile, l'ambito della variabile è probabilmente errato.
Kevin Krumwiede,

5
"I compilatori non riempiono mai" non dovrebbe essere non sempre ?
CodesInChaos,

25

Hai scritto "questa regola non aiuta a trovare i bug, li nasconde solo" - beh, l'obiettivo della regola non è di aiutare a trovare i bug, ma di evitarli . E quando viene evitato un bug, non c'è nulla di nascosto.

Consente di chiarire il problema in termini di esempio: supponiamo che la my_readfunzione abbia il contratto scritto da inizializzare bytes_readin tutte le circostanze, ma non in caso di errore, quindi è difettosa, almeno, per questo caso. L'intenzione è quella di utilizzare l'ambiente di runtime per mostrare quel bug non inizializzando bytes_readprima il parametro. Finché sai per certo che esiste un disinfettante per indirizzi, questo è davvero un modo possibile per rilevare un tale bug. Per correggere l'errore, è necessario modificare la my_readfunzione internamente.

Ma c'è un diverso punto di vista, che è almeno ugualmente valido: il comportamento difettoso emerge solo dalla combinazione di non inizializzare in bytes_readanticipo e chiamare in my_readseguito (con l'attesa che bytes_readviene inizializzata dopo). Questa è una situazione che accadrà spesso nei componenti del mondo reale quando le specifiche scritte per una funzione come my_readnon sono chiare al 100% o addirittura sbagliate sul comportamento in caso di errore. Tuttavia, finché bytes_readviene inizializzato a zero prima della chiamata, il programma si comporta come se l'inizializzazione fosse stata eseguita all'interno my_read, quindi si comporta correttamente, in questa combinazione non esiste alcun bug nel programma.

Quindi la mia raccomandazione che ne consegue è: utilizzare l'approccio non inizializzante solo se

  • si desidera verificare se una funzione o un blocco di codice inizializza un parametro specifico
  • sei sicuro al 100% che la funzione in gioco abbia un contratto in cui è assolutamente sbagliato non assegnare un valore a quel parametro
  • sei sicuro al 100% che l'ambiente possa capirlo

Queste sono condizioni che è possibile organizzare in genere nel codice di test , per un ambiente di utensili specifico.

Nel codice di produzione, tuttavia, è meglio inizializzare sempre in anticipo una tale variabile, è l'approccio più difensivo, che previene i bug nel caso in cui il contratto sia incompleto o errato, o nel caso in cui il disinfettante per indirizzi o simili misure di sicurezza non siano attivate. E la regola "crash-early" si applica, come hai scritto correttamente, se l'esecuzione del programma rileva un bug. Ma quando si inizializza una variabile in anticipo significa che non c'è nulla di sbagliato, quindi non è necessario interrompere l'ulteriore esecuzione.


4
Questo è esattamente quello che stavo pensando quando l'ho letto. Non sta spazzando le cose sotto il tappeto, le sta spazzando nella pattumiera!
corsiKa

22

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 + dataLengthavvolge, sarà più piccolo del putpuntatore (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 ifdell'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+dataLengthnon può mai essere inferiore ptrsenza un overflow del puntatore UB, l' ifistruzione 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.


Per una lettura molto divertente sul comportamento indefinito, software.intel.com/en-us/blogs/2013/01/06/… è un post incredibilmente ben scritto su quanto può andare male. Tuttavia, quel particolare post è sulle operazioni atomiche, che sono molto confuse per la maggior parte, quindi evito di raccomandarlo come primer per UB e come può andare storto.
Cort Ammon - Ripristina Monica il

1
Vorrei che C avesse degli intrinseci per impostare un valore o una matrice di essi su valori indeterminati non inizializzati, non intrappolati, o valori non specificati, o trasformare i valori cattivi in ​​valori meno cattivi (indeterminati non intrappolati o non specificati) lasciando soli valori definiti. I compilatori potrebbero utilizzare tali direttive per favorire utili ottimizzazioni, mentre i programmatori potrebbero usarle per evitare di dover scrivere codice inutile bloccando al contempo le "ottimizzazioni" infranti quando usano cose come le tecniche a matrice sparsa.
supercat il

@supercat Sarebbe una bella caratteristica, supponendo che tu stia prendendo di mira piattaforme dove questa è una soluzione valida. Uno degli esempi di problemi noti è la capacità di creare modelli di memoria non solo non validi per il tipo di memoria, ma impossibili da ottenere con mezzi ordinari. boolè un ottimo esempio in cui ci sono ovvi problemi, ma si presentano altrove a meno che non si presuma che si stia lavorando su una piattaforma molto utile come x86 o ARM o MIPS in cui tutti questi problemi si risolvono al momento dell'opcode.
Cort Ammon - Ripristina Monica il

Considera il caso in cui un ottimizzatore può dimostrare che un valore utilizzato per a switchè inferiore a 8, a causa delle dimensioni dell'aritmetica dei numeri interi, in modo da poter utilizzare istruzioni rapide che presumevano che non vi fosse il rischio di un valore "grande". Improvvisamente un appare un valore non specificato (che non potrebbe mai essere costruito usando le regole del compilatore), facendo qualcosa di inaspettato, e improvvisamente hai un salto enorme dalla fine di una tabella di salto. Consentire qui risultati non specificati significa che ogni istruzione switch nel programma deve avere trappole extra per supportare questi casi che "non possono mai verificarsi".
Cort Ammon - Ripristina Monica il

Se gli intrinseci fossero standardizzati, i compilatori potrebbero essere tenuti a fare tutto il necessario per onorare la semantica; se ad esempio alcuni percorsi di codice impostano una variabile e altri no, e un intrinseco quindi dice "converti in valore non specificato se non inizializzato o indeterminato; altrimenti lascia solo", un compilatore per piattaforme con registri "non a valore" dovrebbe inserisci il codice per inizializzare la variabile prima di qualsiasi percorso di codice o su qualsiasi percorso di codice in cui altrimenti verrebbe persa l'inizializzazione, ma l'analisi semantica richiesta per farlo è piuttosto semplice.
supercat il

5

Lavoro principalmente in un linguaggio di programmazione funzionale in cui non ti è permesso riassegnare le variabili. Mai. Ciò elimina completamente questa classe di bug. All'inizio questa sembrava un'enorme restrizione, ma ti costringe a strutturare il tuo codice in modo coerente con l'ordine in cui apprendi nuovi dati, che tende a semplificare il tuo codice e facilitarne la manutenzione.

Queste abitudini possono essere trasferite anche in lingue imperative. È quasi sempre possibile riformattare il codice per evitare l'inizializzazione di una variabile con un valore fittizio. Ecco cosa ti dicono di fare queste linee guida. Vogliono che tu inserisca qualcosa di significativo, non qualcosa che renderà felici gli strumenti automatici.

Il tuo esempio con un'API in stile C è un po 'più complicato. In quei casi, quando uso la funzione, inizializzerò a zero per evitare che il compilatore si lamenti, ma una volta nei my_readtest unitari, inizializzerò qualcos'altro per assicurarmi che la condizione di errore funzioni correttamente. Non è necessario testare ogni possibile condizione di errore ad ogni utilizzo.


5

No, non nasconde bug. Invece rende deterministico il comportamento in modo tale che se un utente incontra un errore, uno sviluppatore può riprodurlo.


1
E l'inizializzazione con -1 può essere effettivamente significativa. Dove "int bytes_read = 0" è errato, perché in realtà è possibile leggere 0 byte, inizializzandolo con -1 si evince chiaramente che nessun tentativo di leggere byte è riuscito e si può verificare.
Pieter B,

4

TL; DR: Esistono due modi per correggere questo programma, inizializzare le variabili e pregare. Solo uno fornisce risultati coerenti.


Prima di poter rispondere alla tua domanda, dovrò prima spiegare cosa significa Comportamento indefinito . In realtà, lascerò che un autore di compilatore faccia la maggior parte del lavoro:

Se non sei disposto a leggere quegli articoli, un TL; DR è:

Undefined Behavior è un contratto sociale tra lo sviluppatore e il compilatore; il compilatore assume con cieca fede che il suo utente non farà mai e poi mai affidamento su Undefined Behaviour.

L'archetipo di "Demoni che volano dal tuo naso" non è riuscito assolutamente a trasmettere le implicazioni di questo fatto, sfortunatamente. Seppur inteso a dimostrare che qualsiasi cosa potesse succedere, era così assolutamente incredibile che per lo più era scrollato di dosso.

La verità, tuttavia, è che Undefined Behaviour influisce sulla compilazione stessa, molto prima ancora di tentare di utilizzare il programma (strumentato o meno, all'interno di un debugger o meno) e può cambiare completamente il suo comportamento.

Trovo sorprendente l'esempio nella parte 2 sopra:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

si trasforma in:

void contains_null_check(int *P) {
  *P = 4;
}

perché è ovvio che Pnon può essere 0dato che è stato verificato prima di essere verificato.


Come si applica al tuo esempio?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

Bene, hai commesso l'errore comune di presumere che un comportamento indefinito causerebbe un errore di runtime. Non può.

Immaginiamo che la definizione di my_readsia:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

e procedere come previsto da un buon compilatore con inline:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

Quindi, come previsto da un buon compilatore, ottimizziamo i rami inutili:

  1. Nessuna variabile deve essere utilizzata non inizializzata
  2. bytes_readsarebbe usato non inizializzato se resultnon lo fosse0
  3. Lo sviluppatore promette che resultnon lo sarà mai 0!

Quindi resultnon è mai 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, resultnon viene mai usato:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, possiamo rimandare la dichiarazione di bytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Ed eccoci qui, una trasformazione a conferma rigorosa dell'originale, e nessun debugger intrappolerà una variabile non inizializzata perché non ce n'è.

Sono stato su quella strada, capire il problema quando il comportamento e l'assemblaggio previsti non coincidono non è davvero divertente.


A volte penso che i compilatori dovrebbero ottenere il programma per eliminare i file di origine quando eseguono un percorso UB. I programmatori impareranno quindi cosa significa UB per il loro utente finale ....
mattnz,

1

Diamo un'occhiata più da vicino al tuo codice di esempio:

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Questo è un buon esempio Se prevediamo un errore come questo, possiamo inserire la riga assert(bytes_read > 0);e rilevare questo bug in fase di esecuzione, cosa impossibile con una variabile non inizializzata.

Ma supponiamo di no e troviamo un errore all'interno della funzione use(buffer). Carichiamo il programma nel debugger, controlliamo la backtrace e scopriamo che è stato chiamato da questo codice. Quindi inseriamo un punto di interruzione nella parte superiore di questo frammento, eseguiamo di nuovo e riproduciamo il bug. Facciamo un singolo passo nel tentativo di catturarlo.

Se non abbiamo inizializzato bytes_read, contiene immondizia. Non contiene necessariamente la stessa spazzatura ogni volta. Superiamo la linea my_read(buffer, &bytes_read);. Ora, se ha un valore diverso rispetto a prima, potremmo non essere in grado di riprodurre il nostro bug! Potrebbe funzionare la volta successiva, sullo stesso input, per un incidente completo. Se è costantemente zero, otteniamo un comportamento coerente.

Controlliamo il valore, forse anche su una backtrace nella stessa corsa. Se è zero, possiamo vedere che qualcosa non va; bytes_readnon dovrebbe essere zero in caso di successo. (O se può essere, potremmo voler inizializzarlo a -1.) Probabilmente qui possiamo catturare il bug. Se bytes_readè un valore plausibile, però, che sembra essere sbagliato, lo individueremmo a colpo d'occhio?

Ciò è particolarmente vero per i puntatori: un puntatore NULL sarà sempre ovvio in un debugger, può essere testato per molto facilmente e dovrebbe segfault su hardware moderno se proviamo a dereferenziarlo. Un puntatore di immondizia può causare bug di corruzione della memoria non riproducibili in un secondo momento e questi sono quasi impossibili da eseguire il debug.


1

L'OP non si basa su comportamenti indefiniti, o almeno non esattamente. In effetti, fare affidamento su comportamenti indefiniti è male. Allo stesso tempo, il comportamento di un programma in un caso inaspettato è anche indefinito, ma un diverso tipo di indefinito. Se si imposta una variabile a zero, ma non avete intenzione di avere un percorso di esecuzione che usi che lo zero iniziale, sarà il vostro programma si comportano in modo sano quando si dispone di un bug e fai avere un tale percorso? Ora sei tra le erbacce; non hai intenzione di utilizzare quel valore, ma lo stai comunque usando. Forse sarà innocuo, o forse causerà l'arresto anomalo del programma, o forse causerà al programma la corruzione silenziosa dei dati. Non lo sai.

Ciò che l'OP sta dicendo è che ci sono strumenti che ti aiuteranno a trovare questo bug, se glielo permetti. Se non si inizializza il valore, ma lo si utilizza comunque, esistono analizzatori statici e dinamici che indicano che è presente un bug. Un analizzatore statico ti dirà prima ancora di iniziare a testare il programma. Se, d'altra parte, inizializzi ciecamente il valore, gli analizzatori non possono dire che non hai intenzione di utilizzare quel valore iniziale e quindi il tuo bug non viene rilevato. Se sei fortunato è innocuo o semplicemente si blocca il programma; se sei sfortunato corrompe silenziosamente i dati.

L'unico posto in cui non sono d'accordo con l'OP è alla fine, dove dice "quando altrimenti avrebbe già avuto un errore di segmentazione". In effetti, una variabile non inizializzata non produrrà in modo affidabile un errore di segmentazione. Invece, direi che dovresti usare strumenti di analisi statica che non ti permetteranno di tentare di eseguire il programma.


0

Una risposta alla tua domanda deve essere suddivisa in diversi tipi di variabili che compaiono all'interno di un programma:


Variabili locali

Di solito la dichiarazione dovrebbe essere proprio nel punto in cui la variabile ottiene per prima il suo valore. Non dichiarare variabili come nel vecchio stile C:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

Questo elimina il 99% della necessità di inizializzazione, le variabili hanno il loro valore finale fin dall'inizio. Le poche eccezioni sono quelle in cui l'inizializzazione dipende da una condizione:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

Credo che sia una buona idea scrivere questi casi in questo modo:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

I. e. asserire esplicitamente che viene eseguita un'inizializzazione ragionevole della variabile.


Variabili dei membri

Qui sono d'accordo con quello che hanno detto gli altri rispondenti: dovrebbero essere sempre inizializzati dagli elenchi dei costruttori / inizializzatori. In caso contrario, ti impegni a garantire la coerenza tra i tuoi membri. E se si dispone di un insieme di membri che non sembrano necessitare di inizializzazione in tutti i casi, rifattorizzare la propria classe, aggiungendo quei membri in una classe derivata dove sono sempre necessari.


buffer

È qui che non sono d'accordo con le altre risposte. Quando le persone diventano religiose sull'inizializzazione delle variabili, finiscono spesso per inizializzare i buffer in questo modo:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

Credo che questo sia quasi sempre dannoso: l'unico effetto di queste inizializzazioni è che rendono strumenti come valgrindimpotenti. Qualsiasi codice che legge più dai buffer inizializzati di quanto dovrebbe è molto probabilmente un bug. Ma con l'inizializzazione, quel bug non può essere esposto da valgrind. Quindi non usarli a meno che tu non faccia davvero affidamento sul fatto che la memoria sia piena di zeri (e in tal caso, lascia un commento dicendo ciò di cui hai bisogno lo zero).

Consiglio vivamente anche di aggiungere un target al sistema di compilazione che esegue l'intero testuite valgrindo uno strumento simile per esporre bug di utilizzo prima dell'inizializzazione e perdite di memoria. Questo è più prezioso di tutte le preinizializzazioni di variabili. Tale valgrindobiettivo dovrebbe essere eseguito su base regolare, soprattutto prima che qualsiasi codice diventi pubblico.


Variabili globali

Non puoi avere variabili globali che non sono inizializzate (almeno in C / C ++ ecc.), Quindi assicurati che questa inizializzazione sia quella che desideri.


Si Base& b = foo() ? new Derived1 : new Derived2;
noti

@Lorehead Può funzionare per i casi semplici, ma non per quelli più complessi: non vuoi farlo se hai tre o più casi e i tuoi costruttori prendono tre o più argomenti, semplicemente per leggibilità motivi. E questo non sta nemmeno prendendo in considerazione alcun calcolo che potrebbe essere necessario eseguire, come la ricerca di un argomento per un ramo di inizializzazione in un ciclo.
cmaster

Per i casi più complicati, si potrebbe avvolgere il codice di inizializzazione in una funzione di fabbrica: Base &b = base_factory(which);. Questo è molto utile se devi chiamare il codice più di una volta o se ti consente di rendere il risultato una costante.
Davislor,

@Lorehead È vero, e sicuramente la strada da percorrere se la logica richiesta non è semplice. Tuttavia, credo che ci sia una piccola area grigia in mezzo alla quale l'inizializzazione tramite ?:è una PITA e una funzione di fabbrica è ancora eccessiva. Questi casi sono pochi e lontani tra loro, ma esistono.
cmaster

-2

Un compilatore C, C ++ o Objective-C decente con le opzioni di compilatore corrette impostate ti dirà in fase di compilazione se viene utilizzata una variabile prima che sia impostato il suo valore. Poiché in queste lingue l'utilizzo del valore di una variabile non inizializzata è un comportamento indefinito, "impostare un valore prima dell'uso" non è un suggerimento, né una linea guida o una buona pratica, è un requisito del 100%; altrimenti il ​​tuo programma è assolutamente rotto. In altre lingue, come Java e Swift, il compilatore non ti permetterà mai di usare una variabile prima che sia inizializzata.

C'è una differenza logica tra "inizializza" e "imposta un valore". Se voglio trovare il tasso di conversione tra dollari ed euro e scrivere "double rate = 0.0;" quindi la variabile ha un valore impostato, ma non è inizializzato. Lo 0.0 memorizzato qui non ha nulla a che fare con il risultato corretto. In questa situazione, se a causa di un bug non memorizzi mai il tasso di conversione corretto, il compilatore non ha la possibilità di dirtelo. Se hai appena scritto "double rate;" e non ha mai memorizzato un tasso di conversione significativo, te lo direbbe il compilatore.

Quindi: non inizializzare una variabile solo perché il compilatore ti dice che è usato senza essere inizializzato. Questo sta nascondendo un bug. Il vero problema è che stai usando una variabile che non dovresti usare o che su un percorso di codice non hai impostato un valore. Risolvi il problema, non nasconderlo.

Non inizializzare una variabile solo perché il compilatore potrebbe dirti che viene utilizzata senza essere inizializzata. Ancora una volta, stai nascondendo problemi.

Dichiarare le variabili vicine all'uso. Ciò migliora le possibilità che sia possibile inizializzarlo con un valore significativo nel punto di dichiarazione.

Evita di riutilizzare le variabili. Quando riutilizzi una variabile, molto probabilmente viene inizializzata su un valore inutile quando la usi per il secondo scopo.

È stato commentato che alcuni compilatori hanno falsi negativi e che il controllo dell'inizializzazione equivale al problema di arresto. Entrambi sono in pratica irrilevanti. Se un compilatore, come citato, non riesce a trovare l'uso di una variabile non inizializzata dieci anni dopo la segnalazione del bug, allora è tempo di cercare un compilatore alternativo. Java lo implementa due volte; una volta nel compilatore, una volta nel verificatore, senza problemi. Il modo semplice per aggirare il problema di arresto non è richiedere che una variabile sia inizializzata prima dell'uso, ma che sia inizializzata prima dell'uso in un modo che può essere verificato da un algoritmo semplice e veloce.


Questo suona superficialmente buono, ma si basa troppo sull'accuratezza degli avvisi di valore non inizializzato. Ottenere questi perfettamente corretti equivale al problema dell'arresto, e i compilatori di produzione possono e soffrono di falsi negativi (cioè non diagnosticano una variabile non inizializzata quando dovrebbero); vedi per esempio il bug 18501 di GCC , che non è più stato risolto da oltre dieci anni.
zwol,

Quello che dici di gcc è appena stato detto. Il resto è irrilevante.
gnasher729,

È triste per gcc, ma se non capisci perché il resto è rilevante, devi educare te stesso.
zwol,
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.