È ragionevole annullare la protezione di ogni singolo puntatore senza riferimenti?


65

In un nuovo lavoro, sono stato segnalato nelle recensioni di codice per codice come questo:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender) { }

void PowerManager::SignalShutdown()
{
    msgSender_->sendMsg("shutdown()");
}

Mi è stato detto che l'ultimo metodo dovrebbe leggere:

void PowerManager::SignalShutdown()
{
    if (msgSender_) {
        msgSender_->sendMsg("shutdown()");
    }
}

vale a dire, mi deve mettere una NULLguardia attorno alla msgSender_variabile, anche se è un membro di dati privati. È difficile per me trattenermi dall'usare le imprecazioni per descrivere cosa provo per questo pezzo di "saggezza". Quando chiedo una spiegazione, ricevo una litania di storie dell'orrore su come un programmatore junior, qualche anno, si è confuso su come avrebbe dovuto funzionare una classe e cancellato accidentalmente un membro che non avrebbe dovuto (e lo ha impostato in NULLseguito , a quanto pare), e le cose sono esplose sul campo subito dopo il rilascio di un prodotto, e abbiamo "imparato nel modo più duro, fidati di noi" che è meglio NULLcontrollare tutto .

Per me, sembra una programmazione di culto del carico , chiara e semplice. Alcuni colleghi ben intenzionati stanno seriamente cercando di aiutarmi a "ottenerlo" e vedere come questo mi aiuterà a scrivere codice più robusto, ma ... Non posso fare a meno di sentirli come quelli che non lo capiscono .

È ragionevole che uno standard di codifica richieda che ogni singolo puntatore con riferimento a una funzione sia controllato per NULLprimo, anche per i membri di dati privati? (Nota: per dare un po 'di contesto, realizziamo un dispositivo elettronico di consumo, non un sistema di controllo del traffico aereo o qualche altro prodotto "fail-egals-people-die".)

EDIT : nell'esempio sopra, il msgSender_collaboratore non è facoltativo. Se mai NULL, indica un bug. L'unico motivo per cui viene passato nel costruttore è quindi PowerManagerpuò essere testato con una IMsgSendersottoclasse finta .

SINTESI : Ci sono state delle risposte davvero fantastiche a questa domanda, grazie a tutti. Ho accettato quello di @aaronps principalmente per la sua brevità. Sembra esserci un accordo generale abbastanza ampio sul fatto che:

  1. Il mandato di NULLguardia per ogni singolo puntatore senza riferimenti è eccessivo, ma
  2. Puoi fare un passo laterale dell'intero dibattito usando invece un riferimento (se possibile) o un constpuntatore e
  3. assertle dichiarazioni sono un'alternativa più illuminata alle NULLguardie per verificare che siano soddisfatte le condizioni preliminari di una funzione.

14
Non pensare per un minuto che gli errori siano di dominio dei programmatori junior. Il 95% degli sviluppatori di tutti i livelli di esperienza fa una capatina ogni tanto. Il restante 5% giace tra i denti.
Blrfl

56
Se vedessi qualcuno scrivere codice che verifica la presenza di un valore nullo e non riesce in modo silenzioso, vorrei licenziarlo sul posto.
Winston Ewert,

16
Le cose che falliscono silenziosamente sono praticamente le peggiori. Almeno quando esplode, sai dove si verifica l'esplosione. Controllare nulle non fare nulla è solo un modo per spostare il bug sull'esecuzione, rendendo molto più difficile rintracciare la fonte.
MirroredFate

1
+1, domanda molto interessante. Oltre alla mia risposta, vorrei anche sottolineare che quando devi scrivere un codice il cui scopo è quello di ospitare un test unitario, quello che stai realmente facendo è scambiare un rischio aumentato per un falso senso di sicurezza (più righe di codice = più possibilità per i bug). La riprogettazione per utilizzare i riferimenti non solo migliora la leggibilità del codice, ma riduce anche la quantità di codice (e test) che è necessario scrivere per dimostrare che funziona.
Seth,

6
@Rig, sono sicuro che ci sono casi appropriati per un fallimento silenzioso. Ma se qualcuno mette il silenzio in errore come pratica generale (a meno che non lavori in un regno in cui abbia un senso), non voglio davvero lavorare su un progetto che sta inserendo il codice.
Winston Ewert

Risposte:


66

Dipende dal "contratto":

Se PowerManager DEVE avere un valore valido IMsgSender, non verificare mai la presenza di null, lasciarlo morire prima.

Se d'altra parte, può avere un IMsgSender, quindi è necessario controllare ogni volta che si utilizza, così semplice.

Commento finale sulla storia del programmatore junior, il problema è in realtà la mancanza di procedure di test.


4
+1 per una risposta molto concisa che riassume in gran parte il modo in cui mi sento: "dipende ..." Hai anche avuto il coraggio di dire "non controllare mai null" ( se null non è valido). Inserire una funzione assert()in ogni membro che dereferenzia un puntatore è un compromesso che probabilmente placherà molte persone, ma anche per me sembra una "paranoia speculativa". L'elenco di cose al di là della mia capacità di controllo è infinito e una volta che mi permetto di iniziare a preoccuparmi per loro, diventa una pendenza davvero scivolosa. Imparare a fidarmi dei miei test, dei miei colleghi e del mio debugger mi ha aiutato a dormire meglio la notte.
evadeflow,

2
Quando leggi il codice, queste affermazioni possono essere molto utili per capire come dovrebbe funzionare il codice. Non ho mai incontrato una base di codice che mi ha fatto andare "Vorrei che non avesse tutte queste affermazioni dappertutto", ma ho visto molte cose in cui ho detto il contrario.
Kristof Provost,

1
Chiaro e semplice, questa è la risposta corretta alla domanda.
Matthew Azkimov,

2
Questa è la risposta corretta più vicina ma non posso essere d'accordo con 'mai controllare per null', controlla sempre i parametri non validi nel punto in cui verranno assegnati a una variabile membro.
James,

Grazie per i commenti È meglio lasciare che si blocchi prima che qualcuno non legga la specifica e la inizializzi con un valore nullo quando deve avere qualcosa di valido lì.
Aaronps,

77

Sento che il codice dovrebbe leggere:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}

Questo in realtà è meglio che proteggere il NULL, perché rende molto chiaro che la funzione non dovrebbe mai essere chiamata se msgSender_è NULL. Inoltre, ti accorgerai che ciò accadrà.

La "saggezza" condivisa dei tuoi colleghi ignorerà silenziosamente questo errore, con risultati imprevedibili.

In generale, i bug sono più facili da correggere se vengono rilevati più vicini alla loro causa. In questo esempio la protezione NULL proposta porterebbe a un messaggio di arresto che non viene impostato, il che può o meno comportare un bug evidente. Avresti più difficoltà a lavorare all'indietro per la SignalShutdownfunzione che se l'intera applicazione fosse appena morta, producendo un backtrace o un dump core dandy a portata di mano che punta direttamente a SignalShutdown().

È un po 'controintuitivo, ma il crash non appena qualcosa non va tende a rendere il codice più robusto. Questo perché in realtà trovi i problemi e tende ad avere anche cause molto ovvie.


10
La grande differenza tra il richiamo di assert e l'esempio di codice dell'OP è che assert è abilitato solo nelle build di debug (#define NDEBUG) Quando il prodotto entra nelle mani del cliente e il debug è disabilitato e una combinazione mai tracciata di percorsi di codice viene eseguita per uscire il puntatore è nullo anche se la funzione in questione viene eseguita, l'asserzione non fornisce alcuna protezione.
Atk,

17
Presumibilmente avresti testato la build prima di inviarla effettivamente al cliente, ma sì, questo è un possibile rischio. Le soluzioni sono comunque semplici: o semplicemente con le asserzioni abilitate (questa è la versione che hai provato comunque) o sostituiscile con la tua macro che asserisce in modalità debug e solo registri, log + backtrace, uscite, ... in modalità di rilascio.
Kristof Provost,

7
@James - nel 90% dei casi non ti riprenderai dal controllo NULL fallito. Ma in tal caso il codice fallirà silenziosamente e l'errore potrebbe apparire molto più tardi - per esempio hai pensato di aver salvato il file ma in realtà il controllo null a volte fallisce lasciandoti non più saggio. Non è possibile riprendersi da tutte le situazioni che non devono accadere. Puoi verificare che non accadranno, ma non credo che tu abbia un budget a meno che tu non scriva il codice per il satellite della NASA o la centrale nucleare. Devi avere un modo di dire "Non ho idea di cosa stia succedendo - il programma non dovrebbe essere in questo stato".
Maciej Piechotka,

6
@James Non sono d'accordo con il lancio. Questo fa sembrare che sia qualcosa che può effettivamente accadere. Le affermazioni sono per cose che dovrebbero essere impossibili. Per veri bug nel codice in altre parole. Le eccezioni sono quando accadono cose inaspettate, ma possibili. Le eccezioni sono per le cose che potresti concepire gestire e recuperare. Errori logici nel codice da cui non è possibile ripristinare e non si deve provare neanche.
Kristof Provost,

2
@James: Nella mia esperienza con i sistemi embedded, il software e l'hardware sono sempre progettati in modo tale che è estremamente difficile rendere un dispositivo completamente inutilizzabile. Nella maggior parte dei casi, esiste un watchdog hardware che forza un ripristino completo del dispositivo se il software smette di funzionare.
Bart van Ingen Schenau,

30

Se msgSender non deve mai esserlo null, dovresti mettere il nullsegno di spunta solo nel costruttore. Questo vale anche per tutti gli altri input nella classe - metti il ​​controllo di integrità nel punto di entrata nel 'modulo' - classe, funzione ecc.

La mia regola pratica è quella di eseguire controlli di integrità tra i limiti del modulo - la classe in questo caso. Inoltre, una classe dovrebbe essere abbastanza piccola da poter verificare rapidamente mentalmente l'integrità della vita dei membri della classe, garantendo la prevenzione di errori quali eliminazioni improprie / assegnazioni nulle. Il controllo null che viene eseguito nel codice nel tuo post presuppone che qualsiasi utilizzo non valido assegni effettivamente null al puntatore, il che non è sempre il caso. Poiché l '"utilizzo non valido" implica intrinsecamente che non si applicano ipotesi sul codice normale, non possiamo essere certi di rilevare tutti i tipi di errori del puntatore, ad esempio una cancellazione, un incremento non validi, ecc.

Inoltre, se si è certi che l'argomento non può mai essere nullo, prendere in considerazione l'utilizzo di riferimenti, a seconda dell'utilizzo della classe. Altrimenti, considera l'utilizzo std::unique_ptro std::shared_ptral posto di un puntatore non elaborato.


11

No, non è ragionevole controllare la dereferenza di ogni puntatore per l'essere puntatore NULL.

I controlli con puntatore nullo sono utili per gli argomenti delle funzioni (inclusi gli argomenti del costruttore) per garantire che siano soddisfatte le condizioni preliminari o per intraprendere azioni appropriate se non viene fornito un parametro facoltativo e sono utili per verificare l'invariante di una classe dopo aver esposto gli interni della classe. Ma se l'unica ragione per cui un puntatore è diventato NULL è l'esistenza di un bug, allora non ha senso controllare. Quel bug avrebbe potuto facilmente impostare il puntatore su un altro valore non valido.

Se dovessi affrontare una situazione come la tua, farei due domande:

  • Perché l'errore di quel programmatore junior non è stato colto prima dell'uscita? Ad esempio in una revisione del codice o nella fase di test? Portare sul mercato un dispositivo di elettronica di consumo difettoso può essere altrettanto costoso (se si tiene conto della quota di mercato e dell'avviamento) del rilascio di un dispositivo difettoso utilizzato in un'applicazione critica per la sicurezza, quindi mi aspetto che la società si occupi seriamente dei test e altre attività di controllo qualità.
  • Se il controllo null fallisce, quale gestione degli errori ti aspetti che io metta in atto, o potrei semplicemente scrivere al assert(msgSender_)posto del controllo null? Se hai appena effettuato il check-in null, potresti aver impedito un arresto anomalo, ma potresti aver creato una situazione peggiore perché il software continua con la premessa che è stata eseguita un'operazione mentre in realtà quell'operazione è stata saltata. Ciò potrebbe causare l'instabilità di altre parti del software.

9

Questo esempio sembra riguardare più la durata dell'oggetto che il fatto che un parametro di input sia nullo †. Dato che dici che PowerManagerdeve sempre avere un valore valido IMsgSender, passare l'argomento per puntatore (permettendo così la possibilità di un puntatore nullo) mi colpisce come un difetto di progettazione ††.

In situazioni come questa, preferirei cambiare l'interfaccia in modo che i requisiti del chiamante vengano applicati dalla lingua:

PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}

Riscrivendolo in questo modo si dice che PowerManagerdeve contenere un riferimento IMsgSenderper tutta la sua vita. Questo, a sua volta, stabilisce anche un requisito implicito che IMsgSenderdeve vivere più a lungo di PowerManager, e nega la necessità di eventuali controlli o asserzioni di puntatori nulli all'interno PowerManager.

Puoi anche scrivere la stessa cosa usando un puntatore intelligente (tramite boost o c ++ 11), per forzare esplicitamente IMsgSendera vivere più a lungo di PowerManager:

PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}

Questo metodo è preferito se è possibile che IMsgSendernon si possa garantire che la durata della vita sia più lunga di PowerManager(cioè, x = new IMsgSender(); p = new PowerManager(*x);).

† Per quanto riguarda i puntatori: il controllo null dilagante rende il codice più difficile da leggere e non migliora la stabilità (migliora l' aspetto della stabilità, che è molto peggio).

Da qualche parte, qualcuno ha un indirizzo per la memoria per contenere il IMsgSender. È responsabilità di quella funzione assicurarsi che l'allocazione abbia avuto esito positivo (controllo dei valori di ritorno della libreria o gestione corretta delle std::bad_alloceccezioni), in modo da non passare puntatori non validi.

Dal momento che il PowerManagernon possiede IMsgSender(lo sta solo prendendo in prestito per un po '), non è responsabile per l'allocazione o la distruzione di quella memoria. Questo è un altro motivo per cui preferisco il riferimento.

†† Dato che sei nuovo in questo lavoro, mi aspetto che stai hackerando il codice esistente. Quindi, per difetto di progettazione, intendo che il difetto è nel codice con cui stai lavorando. Pertanto, le persone che stanno segnalando il tuo codice perché non sta verificando la presenza di puntatori null si stanno davvero segnalando per la scrittura di codice che richiede puntatori :)


1
A volte non c'è nulla di sbagliato nell'usare puntatori non elaborati. Std :: shared_ptr non è un proiettile d'argento e usarlo in un elenco di argomenti è una cura per l'ingegneria sciatta anche se mi rendo conto che è considerato 'leet' usarlo quando possibile. Usa java se ti senti così.
James,

2
Non ho detto che i puntatori grezzi sono cattivi! Hanno sicuramente il loro posto. Tuttavia, questo è C + + , i riferimenti (e i puntatori condivisi, ora) fanno parte del linguaggio per un motivo. Usali, ti faranno avere successo.
Seth,

Mi piace l'idea del puntatore intelligente, ma non è un'opzione in questo particolare concerto. +1 per raccomandare l'uso di riferimenti (come hanno fatto @Max e pochi altri). A volte non è possibile iniettare un riferimento, tuttavia, a causa di problemi di dipendenza da galline e uova, cioè se un oggetto che sarebbe altrimenti candidato all'iniezione deve avere un puntatore (o riferimento) al suo titolare iniettato in esso . Questo a volte può indicare un design strettamente accoppiato; ma è spesso accettabile, come nella relazione tra un oggetto e il suo iteratore, dove non ha mai senso usare quest'ultimo senza il primo.
evadeflow,

8

Come le eccezioni, le condizioni di guardia sono utili solo se si sa cosa fare per recuperare l'errore o se si desidera fornire un messaggio di eccezione più significativo.

La deglutizione di un errore (che venga rilevato come un'eccezione o un controllo di sicurezza) è solo la cosa giusta da fare quando l'errore non ha importanza. Il posto più comune per me per vedere gli errori che vengono ingoiati è nel codice di registrazione degli errori - non vuoi mandare in crash un'app perché non sei riuscito a registrare un messaggio di stato.

Ogni volta che viene chiamata una funzione, e non è un comportamento opzionale, dovrebbe fallire rumorosamente, non in silenzio.

Modifica: pensando alla storia del tuo programmatore junior sembra che quello che è successo è stato che un membro privato è stato impostato su null quando ciò non avrebbe mai dovuto accadere. Hanno avuto un problema con la scrittura inappropriata e stanno tentando di risolverlo convalidando dopo aver letto. Questo è al contrario. Quando lo identifichi, l'errore si è già verificato. La soluzione esecutiva per la revisione del codice / compilatore non è quella delle condizioni di guardia, ma piuttosto di getter e setter o membri const.


7

Come altri hanno notato, questo dipende dal fatto che msgSenderpossa essere legittimamente o meno NULL. Quanto segue presuppone che non dovrebbe mai essere NULL.

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

La "correzione" proposta dagli altri membri della tua squadra viola il principio Tell No Lies dei Dead Program . Gli insetti sono davvero difficili da trovare così com'è. Un metodo che modifica silenziosamente il suo comportamento in base a un problema precedente, non solo rende difficile trovare il primo bug, ma aggiunge anche un secondo bug.

Il giovane ha provocato il caos non controllando nulla. Che cosa succede se questo pezzo di codice provoca il caos continuando a funzionare in uno stato indefinito (il dispositivo è acceso ma il programma "pensa" che è spento)? Forse un'altra parte del programma farà qualcosa che è sicuro solo quando il dispositivo è spento.

Entrambi questi approcci eviteranno guasti silenziosi:

  1. Usa le affermazioni come suggerito da questa risposta , ma assicurati che siano attivate nel codice di produzione. Ciò, naturalmente, potrebbe causare problemi se altri asserzioni fossero scritte supponendo che sarebbero state fuori produzione.

  2. Genera un'eccezione se è nulla.


5

Sono d'accordo con l'intrappolamento del null nel costruttore. Inoltre, se il membro viene dichiarato nell'intestazione come:

IMsgSender* const msgSender_;

Quindi il puntatore non può essere modificato dopo l'inizializzazione, quindi se andava bene durante la costruzione andrà bene per la durata dell'oggetto che lo contiene. (L'oggetto puntato di sarà non essere const.)


Questo è un ottimo consiglio, grazie! Così buono, in effetti, che mi dispiace di non poter votare più in alto. Non è una risposta diretta alla domanda posta, ma è un ottimo modo per eliminare ogni possibilità che il puntatore diventi 'accidentalmente' NULL.
evadeflow

@evadeflow Ho pensato che "sono d'accordo con tutti gli altri" ha risposto alla domanda principale della domanda. Saluti però! ;-)
Grimm The Opiner

Sarebbe un po 'scortese da parte mia cambiare la risposta accettata a questo punto, dato il modo in cui la domanda è stata formulata. Ma penso davvero che questo consiglio sia eccezionale, e mi dispiace non averci pensato da solo. Con un constpuntatore, non c'è bisogno assert()di uscire dal costruttore, quindi questa risposta sembra anche un po 'migliore di quella di @Kristof Provost (che era anche eccezionale, ma ha affrontato la domanda in qualche modo anche indirettamente). Spero che altri votino questo, dal momento che davvero non ha senso assertin ogni metodo quando puoi semplicemente fare il puntatore const.
evadeflow,

@evadeflow Le persone visitano Questins solo per il rappresentante, ora è stato risposto che nessuno lo guarderà più. ;-) Sono entrato solo perché era una domanda sui puntatori e tengo d'occhio il sanguinario "Usa i puntatori intelligenti per tutto sempre".
Grimm The Opiner,

3

Questo è assolutamente pericoloso!

Ho lavorato sotto uno sviluppatore senior in una base di codice C con gli "standard" più scadenti che hanno spinto per la stessa cosa, per controllare ciecamente tutti i puntatori per null. Lo sviluppatore finirebbe per fare cose del genere:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

Una volta ho provato a rimuovere un tale controllo del presupposto una volta in tale funzione e sostituirlo con un assertper vedere cosa sarebbe successo.

Con mio orrore, ho trovato migliaia di righe di codice nella base di codice che stavano passando null a questa funzione, ma in cui gli sviluppatori, probabilmente confusi, hanno lavorato e hanno aggiunto altro codice fino a quando le cose non hanno funzionato.

Con mio ulteriore orrore, ho scoperto che questo problema era prevalente in tutti i tipi di posti nel codebase che controllavano i null. La base di codice è cresciuta nel corso di decenni per arrivare a fare affidamento su questi controlli al fine di poter violare silenziosamente anche i presupposti più esplicitamente documentati. Rimuovendo questi micidiali controlli a favore asserts, tutti gli errori umani logici nel corso dei decenni nella base di codice sarebbero stati rivelati e ci saremmo annegati.

Ci sono volute solo due righe di codice apparentemente innocenti come questo + tempo e una squadra per finire per mascherare mille bug accumulati.

Questi sono i tipi di pratiche che fanno sì che i bug dipendano da altri bug esistenti affinché il software funzioni. È uno scenario da incubo . Inoltre, fa sì che ogni errore logico relativo alla violazione di tali precondizioni compaia misteriosamente un milione di righe di codice lontano dal sito reale in cui si è verificato l'errore, poiché tutti questi controlli null nascondono solo il bug e nascondono il bug fino a raggiungere un luogo che ha dimenticato per nascondere il bug.

Controllare semplicemente i null alla cieca in ogni luogo in cui un puntatore nullo viola una condizione preliminare è, per me, assoluta follia, a meno che il tuo software non sia così critico nei confronti di errori di asserzione e crash di produzione che il potenziale di questo scenario è preferibile.

È ragionevole che uno standard di codifica richieda che ogni singolo puntatore con riferimento a una funzione sia controllato prima per NULL, anche per i membri di dati privati?

Quindi direi, assolutamente no. Non è nemmeno "sicuro". Potrebbe benissimo essere l'opposto e mascherare tutti i tipi di bug in tutta la tua base di codice che, nel corso degli anni, può portare agli scenari più orribili.

assertè la strada da percorrere qui. Le violazioni delle condizioni preliminari non dovrebbero passare inosservate, altrimenti la legge di Murphy può facilmente intervenire.


1
"assert" è la strada da percorrere - ma ricorda che su qualsiasi sistema moderno, ogni accesso alla memoria attraverso un puntatore ha un'asserzione null-pointer incorporata nell'hardware. Quindi in "assert (p! = NULL); return * p;" anche l'assert è per lo più inutile.
gnasher729,

@ gnasher729 Ah, questo è un ottimo punto, ma tendo a vedere assertsia un meccanismo di documentazione per stabilire quali siano i presupposti sia non solo un modo per forzare un aborto. Ma mi piace anche la natura dell'errore di asserzione che mostra esattamente quale riga di codice ha causato un errore e la condizione di asserzione fallita ("documentazione dell'incidente"). Può tornare utile se finiamo per usare una build di debug al di fuori di un debugger e veniamo colti alla sprovvista.

@ gnasher729 O potrei aver frainteso una parte di esso. Questi sistemi moderni mostrano quale linea di codice sorgente in cui l'asserzione è fallita? Immagino generalmente che abbiano perso le informazioni a quel punto e che potessero semplicemente mostrare una violazione di accesso / segfault, ma sono un po 'tutt'altro che "moderno" - ancora testardamente su Windows 7 per la maggior parte del mio sviluppo.

Ad esempio, su MacOS X e iOS, se l'app si arresta in modo anomalo a causa dell'accesso a un puntatore null durante lo sviluppo, il debugger ti dirà la riga esatta di codice in cui si verifica. C'è un altro aspetto strano: il compilatore proverà ad avvisarti se fai qualcosa di sospetto. Se si passa un puntatore a una funzione e si accede ad esso, il compilatore non ti avvertirà che il puntatore potrebbe essere NULL, perché accade così spesso, verrai annegato negli avvisi. Tuttavia, se fai un confronto if (p == NULL) ... allora il compilatore presume che ci sia una buona probabilità che p sia NULL,
gnasher729

perché perché l'avresti testato altrimenti, quindi ti darà un avvertimento da allora in poi se lo usi senza test. Se usi la tua macro assertiva, devi essere un po 'intelligente per scriverla in modo tale che "my_assert (p! = NULL," È un puntatore nullo, stupido! "); * P = 1;" non ti dà un avvertimento.
gnasher729,

2

Objective-C , ad esempio, considera ogni chiamata di metodo su un niloggetto come un no-op che valuta un valore zero ish. Ci sono alcuni vantaggi in quella decisione progettuale in Objective-C, per i motivi suggeriti nella tua domanda. Il concetto teorico di protezione nulla di ogni chiamata di metodo ha qualche merito se è ben pubblicizzato e applicato in modo coerente.

Detto questo, il codice vive in un ecosistema, non in un vuoto. Il comportamento con protezione nulla sarebbe non idiomatico e sorprendente in C ++ e quindi dovrebbe essere considerato dannoso. In sintesi, nessuno scrive il codice C ++ in quel modo, quindi non farlo! Come contro-esempio, tuttavia, si noti che chiamare free()o deletesu un NULLin C e C ++ è garantito per essere un no-op.


Nel tuo esempio, probabilmente varrebbe la pena inserire un'asserzione nel costruttore che msgSendersia nulla. Se il costruttore chiama un metodo su msgSendersubito, quindi tale asserzione sarebbe necessario, dal momento che potrebbe andare in crash proprio lì comunque. Tuttavia, dal momento che sta semplicemente memorizzando msgSender per un uso futuro, non sarebbe ovvio guardando una traccia dello stack di SignalShutdown()come il valore è diventato NULL, quindi un'asserzione nel costruttore renderebbe il debugging significativamente più semplice.

Ancora meglio, il costruttore dovrebbe accettare un const IMsgSender&riferimento, che non può essere NULL.


1

Il motivo per cui ti viene chiesto di evitare dereferenze nulle è garantire che il tuo codice sia solido. Esempi di programmatori junior molto tempo fa sono solo esempi. Chiunque può infrangere il codice per errore e causare una dereferenza nulla, specialmente per i globi globali e di classe. In C e C ++ è ancora più possibile accidentalmente, con la capacità di gestione della memoria diretta. Potresti essere sorpreso, ma questo genere di cose succede molto spesso. Anche da sviluppatori molto competenti, di grande esperienza e molto senior.

Non è necessario null controllare tutto, ma è necessario proteggersi da dereferenze che hanno una discreta probabilità di essere nulle. Questo è comunemente quando sono allocati, usati e non sottoposti a riferimenti in diverse funzioni. È possibile che una delle altre funzioni venga modificata e interrompa la tua funzione. È anche possibile che una delle altre funzioni possa essere chiamata fuori servizio (come se si dispone di un deallocatore che può essere chiamato separatamente dal distruttore).

Preferisco l'approccio che i tuoi colleghi ti stanno dicendo in combinazione con l'utilizzo di assert. Arresto anomalo nell'ambiente di test, quindi è più ovvio che esiste un problema da risolvere e un errore nella produzione.

Dovresti anche utilizzare uno strumento di correttezza del codice come copertura o fortificazione. E dovresti rispondere a tutti gli avvisi del compilatore.

Modifica: come altri hanno già detto, fallire silenziosamente, come in molti degli esempi di codice, è generalmente anche la cosa sbagliata. Se la funzione non può essere ripristinata dal valore null, dovrebbe restituire un errore (o generare un'eccezione) al chiamante. Il chiamante è responsabile della correzione dell'ordine di chiamata, del recupero o della restituzione di un errore (o dell'eccezione) al chiamante e così via. Alla fine, o una funzione è in grado di ripristinare e passare con garbo, ripristinare e fallire con grazia (come un database che fallisce una transazione a causa di un errore interno per un utente ma che non esce inavvertitamente), oppure la funzione determina che lo stato dell'applicazione è corrotto e irrecuperabile e l'applicazione esce.


+1 per avere il coraggio di sostenere una posizione (apparentemente) impopolare. Sarei stato deluso se nessuno avesse difeso i miei colleghi su questo (sono persone molto intelligenti che hanno prodotto un prodotto di qualità per molti anni). Mi piace anche l'idea che le app dovrebbero "arrestarsi in modo anomalo nell'ambiente di test ... e fallire con garbo nella produzione". Non sono convinto che imporre NULLcontrolli 'rote' sia il modo per farlo, ma apprezzo sentire un'opinione da qualcuno al di fuori del mio posto di lavoro che sta sostanzialmente dicendo: "Aigh. Non sembra troppo irragionevole".
evadeflow,

Mi annoio quando vedo le persone testare per NULL dopo malloc (). Mi dice che non capiscono come i moderni sistemi operativi gestiscono la memoria.
Kristof Provost,

2
La mia ipotesi è che @KristofProvost stia parlando di sistemi operativi che usano il sovraccarico, per i quali malloc ha sempre successo (ma il sistema operativo potrebbe successivamente terminare il processo se in realtà non c'è abbastanza memoria disponibile). Tuttavia, questo non è un buon motivo per saltare i controlli null su malloc: l'overcommit non è universale su tutte le piattaforme e anche se è abilitato, potrebbero esserci altri motivi per cui malloc fallisce (ad esempio, su Linux, un limite di spazio degli indirizzi di processo impostato con ulimit ).
John Bartholomew,

1
Quello che ha detto John. Stavo davvero parlando di overcommit. Overcommit è abilitato di default su Linux (che è quello che paga le mie bollette). Non lo so, ma sospetto di sì, se è lo stesso su Windows. Nella maggior parte dei casi finirai per andare in crash, a meno che tu non sia pronto a scrivere un sacco di codice che non sarà mai, mai testato per gestire errori che quasi sicuramente non accadranno mai ...
Kristof Provost

2
Consentitemi di aggiungere che non ho mai visto codice (al di fuori del kernel di Linux, dove la memoria è effettivamente realisticamente possibile) che gestirà correttamente una condizione di memoria insufficiente. Tutto il codice che ho visto provare sarebbe comunque andato in crash più tardi. Tutto ciò che è stato realizzato è stato quello di perdere tempo, rendere il codice più difficile da comprendere e nascondere il vero problema. Le dereferenze null sono facili da eseguire il debug (se si dispone di un file core).
Kristof Provost,

1

L'uso di un puntatore invece di un riferimento mi direbbe che msgSenderè solo un'opzione e qui il controllo null sarebbe corretto. Lo snippet di codice è troppo lento per decidere. Forse ci sono altri elementi PowerManagerche sono preziosi (o testabili) ...

Quando si sceglie tra puntatore e riferimento, peso entrambe le opzioni. Se devo usare un puntatore per un membro (anche per i membri privati), devo accettare la if (x)procedura ogni volta che la dereferenziare.


Come ho già detto in un altro commento da qualche parte, ci sono momenti in cui l'iniezione di un riferimento non è possibile. Un esempio in cui mi
imbatto

@evadeflow: OK, lo confesso, dipende dalla dimensione della classe e dalla visibilità dei membri del puntatore (che non possono essere fatti riferimenti), sia che io controlli prima di dereferenziare. Nei casi in cui la classe è piccola e semplice e i puntatori sono privati, non ...
Wolf

0

Dipende da cosa vuoi che accada.

Hai scritto che stai lavorando a "un dispositivo di elettronica di consumo". Se, in qualche modo, viene introdotto un bug impostando msgSender_su NULL, vuoi

  • il dispositivo da portare avanti, saltando SignalShutdownma continuando il resto del suo funzionamento, oppure
  • il dispositivo si arresta in modo anomalo, costringendo l'utente a riavviarlo?

A seconda dell'impatto del segnale di arresto non inviato, l'opzione 1 potrebbe essere una scelta praticabile. Se l'utente può continuare ad ascoltare la sua musica, ma il display mostra ancora il titolo della traccia precedente, potrebbe essere preferibile un arresto anomalo completo del dispositivo.

Naturalmente, se si sceglie l'opzione 1, un assert(come raccomandato da altri) è fondamentale per ridurre la probabilità che un tale bug si insinui inosservato durante lo sviluppo. La ifprotezione nulla è lì solo per la mitigazione dei guasti nell'uso della produzione.

Personalmente, preferisco anche l'approccio "crash early" per build di produzione, ma sto sviluppando un software aziendale che può essere risolto e aggiornato facilmente in caso di bug. Per i dispositivi elettronici di consumo, questo potrebbe non essere così semplice.

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.