Perché Clang / LLVM mi avvisa dell'utilizzo di default in un'istruzione switch in cui sono coperti tutti i casi elencati?


34

Considera la seguente enum e switch statement:

typedef enum {
    MaskValueUno,
    MaskValueDos
} testingMask;

void myFunction(testingMask theMask) {
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
};

Sono un programmatore di Objective-C, ma l'ho scritto in puro C per un pubblico più ampio.

Clang / LLVM 4.1 con -Weverything mi avvisa sulla riga predefinita:

Etichetta predefinita nello switch che copre tutti i valori di enumerazione

Ora posso capire perché questo è lì: in un mondo perfetto, gli unici valori che entrano nell'argomento theMasksarebbero nell'enum, quindi non è necessario alcun default. Ma cosa succede se arriva qualche hack e lancia un int non inizializzato nella mia bella funzione? La mia funzione verrà fornita come drop nella libreria e non ho alcun controllo su cosa potrebbe andare lì. L'utilizzo defaultè un modo molto semplice di gestirlo.

Perché gli dei LLVM ritengono questo comportamento indegno del loro dispositivo infernale? Dovrei essere preceduto da un'istruzione if per verificare l'argomento?


1
Dovrei dire che la mia ragione per tutto è di farmi un programmatore migliore. Come il NSHipster dice : "Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode.".
Swizzlr,

2
-Weverythingpuò essere utile, ma fai attenzione a non modificare troppo il codice per gestirlo. Alcuni di questi avvertimenti non sono solo inutili ma controproducenti e sono meglio disattivati. (In effetti, questo è il caso d'uso di -Weverything: iniziare con questo e disattivare ciò che non ha senso.)
Steven Fisher,

Potresti espandere un po 'il messaggio di avviso per i posteri? Di solito c'è di più in loro.
Steven Fisher,

Dopo aver letto le risposte, preferisco ancora la tua soluzione iniziale. L'uso dell'istruzione predefinita IMHO è migliore delle alternative fornite nelle risposte. Solo una piccola nota: le risposte sono davvero buone e istruttive, mostrano un modo fantastico per risolvere il problema.
bbaja42,

@StevenFisher Quello era l'intero avvertimento. E come ha sottolineato Killian se modifico il mio enum in seguito, si aprirà la possibilità che valori validi passino all'implementazione predefinita. Sembra essere un buon modello di progettazione (se è una cosa del genere).
Swizzlr,

Risposte:


31

Ecco una versione che non soffre né della segnalazione del clang del problema né di quella a cui stai proteggendo:

void myFunction(testingMask theMask) {
    assert(theMask == MaskValueUno || theMask == MaskValueDos);
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
}

Killian ha già spiegato perché Clang emette l'avvertimento: se estendi l'enum, cadi nel caso predefinito che probabilmente non è quello che vuoi. La cosa corretta da fare è rimuovere il caso predefinito e ottenere avvisi per condizioni non gestite .

Ora sei preoccupato che qualcuno possa chiamare la tua funzione con un valore esterno all'enumerazione. Sembra non riuscire a soddisfare il prerequisito della funzione: è documentato che si aspetta un valore testingMaskdall'enumerazione ma il programmatore ha superato qualcos'altro. Quindi fai un errore del programmatore usando assert()(o NSCAssert()come hai detto che stai usando Objective-C). Fai in modo che il tuo programma si blocchi con un messaggio che spieghi che il programmatore lo sta facendo male, se il programmatore lo fa male.


6
+1. Come piccola modifica, preferisco avere ogni caso return;e aggiungere un assert(false);alla fine (invece di ripetermi elencando gli enumerazioni legali in una iniziale assert()e in switch).
Josh Kelley,

1
Cosa succede se l'enum non corretto non viene passato da un programmatore stupido, ma piuttosto da un bug di corruzione della memoria? Quando il guidatore preme la rottura della loro Toyota e un bug ha corrotto l'enum, allora il programma di gestione delle rotture dovrebbe andare in crash e bruciare, e dovrebbe esserci un testo visualizzato al guidatore: "il programmatore sta sbagliando, programmatore cattivo! ". Non vedo come questo possa aiutare l'utente, prima che guidi oltre il bordo di una scogliera.

2
@Lundin è quello che sta facendo l'OP, o è un inutile caso limite teorico che hai appena costruito? Indipendentemente da ciò, un "bug di corruzione della memoria" [i] è un errore del programmatore, [ii] non è qualcosa da cui puoi continuare in modo significativo (almeno non con i requisiti indicati nella domanda).

1
@GrahamLee Sto solo dicendo che "sdraiati e muori" non è necessariamente l'opzione migliore quando noti un errore imprevisto.

1
Ha il nuovo problema che potresti lasciare che l'asserzione e i casi si sincronizzino accidentalmente.
user253751

9

Avere defaultun'etichetta qui è un indicatore del fatto che sei confuso su ciò che ti aspetti. Dal momento che hai esaurito enumesplicitamente tutti i possibili valori, defaultnon è possibile eseguirli e non è nemmeno necessario proteggerti da eventuali cambiamenti futuri, perché se estendi il enum, il costrutto genererebbe già un avviso.

Quindi, il compilatore nota che hai coperto tutte le basi ma sembra pensare di non averlo fatto, e questo è sempre un brutto segno. Impegnando il minimo sforzo per modificare il switchmodulo previsto, si dimostra al compilatore che ciò che sembra fare è ciò che si sta effettivamente facendo, e lo si conosce.


1
Ma la mia preoccupazione è una variabile sporca e guardinga da ciò (come, come ho detto, un int non inizializzato). Mi sembra che sarebbe possibile che l'istruzione switch raggiunga il valore predefinito in una situazione del genere.
Swizzlr,

Non conosco la definizione di Objective-C a memoria, ma ho ipotizzato che un compilatore typdef'd sarebbe stato applicato dal compilatore. In altre parole, un valore "non inizializzato" potrebbe inserire il metodo solo se il programma mostra già un comportamento indefinito, e trovo del tutto ragionevole che un compilatore non ne tenga conto.
Kilian Foth,

3
@KilianFoth no, non lo sono. Gli enumerativi Objective-C sono enumerazioni C, non enumerazioni Java. Qualsiasi valore dal tipo integrale sottostante potrebbe essere presente nell'argomento della funzione.

2
Inoltre, gli enum possono essere impostati in runtime, da numeri interi. Quindi possono contenere qualsiasi valore immaginabile.

OP è corretto. testingMask m; myFunction (m); molto probabilmente colpirà il caso predefinito.
Matthew James Briggs,

4

Clang è confuso, avendo una dichiarazione di default c'è una pratica perfettamente valida , è conosciuta come programmazione difensiva ed è considerata una buona pratica di programmazione (1). È molto usato nei sistemi mission-critical, anche se forse non nella programmazione desktop.

Lo scopo della programmazione difensiva è quello di catturare errori imprevisti che in teoria non accaderebbero mai. Un tale errore imprevisto non è necessariamente il programmatore che dà alla funzione un input errato, o anche un "hack malefico". Più probabilmente, potrebbe essere causato da una variabile corrotta: overflow del buffer, overflow dello stack, codice in fuga e bug simili non correlati alla funzione potrebbero causare questo. E nel caso di sistemi embedded, le variabili potrebbero cambiare a causa dell'IME, in particolare se si utilizzano circuiti RAM esterni.

Per quanto riguarda cosa scrivere all'interno dell'istruzione predefinita ... se sospetti che il programma sia andato in tilt una volta finito lì, allora hai bisogno di una sorta di gestione degli errori. In molti casi probabilmente puoi semplicemente aggiungere un'istruzione vuota con un commento: "inaspettato ma non importa", ecc., Per dimostrare che hai pensato alla situazione improbabile.


(1) MISRA-C: 2004 15.3.


1
Per favore, non che il programmatore di PC medio in genere trovi il concetto di programmazione difensiva completamente alieno, poiché la loro visione di un programma è un'utopia astratta in cui nulla che possa essere sbagliato in teoria andrà storto nella pratica. Pertanto otterrai risposte abbastanza diverse alla tua domanda a seconda di chi chiedi.

3
Clang non è confuso; è stato esplicitamente chiesto di fornire tutti gli avvisi, inclusi quelli che non sono sempre utili, come gli avvisi stilistici. (Opto personalmente in questo avviso perché non voglio valori predefiniti nei miei switch enum; averli significa che non ricevo un avviso se aggiungo un valore enum e non lo gestisco. Per questo motivo, gestisco sempre il caso di valore negativo al di fuori dell'enum.)
Jens Ayton,

2
@JensAyton È confuso. Tutti gli strumenti di analisi statica professionale e tutti i compilatori conformi MISRA daranno un avviso se un'istruzione switch manca di un'istruzione predefinita. Provane uno tu stesso.

4
La codifica per MISRA-C non è l'unico uso valido per un compilatore C e, di nuovo, -Weverythingattiva ogni avviso, non una selezione appropriata per un caso d'uso specifico. Non soddisfare i tuoi gusti non è la stessa cosa della confusione.
Jens Ayton,

2
@Lundin: Sono abbastanza sicuro che attenersi a MISRA-C non rende magicamente i programmi sicuri e privi di bug ... e sono altrettanto sicuro che ci sia un codice C sicuro e privo di bug da persone che non hanno mai nemmeno sentito parlare di MISRA. In realtà, è poco più dell'opinione di qualcuno (popolare, ma non indiscussa), e ci sono persone che trovano alcune delle sue regole arbitrarie e talvolta persino dannose al di fuori del contesto per cui sono state progettate.
cHao,

4

Meglio ancora:

typedef enum {
    MaskValueUno,
    MaskValueDos,

    MaskValue_count
} testingMask;

void myFunction(testingMask theMask) {
    assert(theMask >= 0 && theMask<MaskValue_count);
    switch theMask {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
};

Questo è meno soggetto a errori quando si aggiungono elementi all'enum. È possibile saltare il test per> = 0 se si rendono i valori enum non firmati. Questo metodo funziona solo se non ci sono lacune nei valori di enum, ma spesso è così.


2
Questo sta inquinando il tipo con valori che semplicemente non vuoi che il tipo contenga. Ha somiglianze con il buon vecchio WTF qui: thedailywtf.com/articles/What_Is_Truth_0x3f_
Martin Sugioarto,

4

Ma cosa succede se arriva qualche hack e getta un int non inizializzato nella mia bella funzione?

Quindi ottieni un comportamento indefinito e la tua defaultvolontà sarà insignificante. Non c'è niente che tu possa fare per migliorare.

Vorrei essere più chiaro. Nel momento in cui qualcuno passa un non inizializzato intnella tua funzione, è un comportamento indefinito. La tua funzione potrebbe risolvere il problema di interruzione e non avrebbe importanza. È UB. Non c'è nulla che tu possa fare una volta che UB è stato invocato.


3
Che ne dici di registrarlo o lanciare un'eccezione invece se ignorarlo silenziosamente?
jgauffin,

3
In effetti, c'è molto che si può fare, come ... aggiungere un'istruzione predefinita allo switch e gestire con grazia l'errore. Questo è noto come programmazione difensiva . Proteggerà non solo dal programmatore stupido che gestisce gli input errati della funzione, ma anche da vari bug causati da overflow del buffer, overflow dello stack, codice in fuga ecc.

1
No, non gestirà nulla. È UB. Il programma ha UB nel momento in cui la funzione viene inserita e il corpo della funzione non può farci nulla.
DeadMG

2
@DeadMG Un enum può avere qualsiasi valore che potrebbe avere il suo tipo intero corrispondente nell'implementazione. Non c'è nulla di indefinito. L'accesso al contenuto di una variabile automatica non inizializzata è in effetti UB, ma "l'hack male" (che è più probabilmente una specie di bug) potrebbe anche lanciare un valore ben definito nella funzione, anche se non è uno dei valori elencato nella dichiarazione enum.

2

L'istruzione predefinita non sarebbe necessariamente di aiuto. Se lo switch si trova su un enum, qualsiasi valore non definito nell'enum finirà per eseguire un comportamento indefinito.

Per quanto ne sai, il compilatore può compilare quel parametro (con il valore predefinito) come:

if (theMask == MaskValueUno)
  // Execute something MaskValueUno code
else // theMask == MaskValueDos
  // Execute MaskValueDos code

Una volta attivato un comportamento indefinito, non è più possibile tornare indietro.


2
Innanzitutto, si tratta di un valore non specificato , non di un comportamento non definito. Ciò significa che il compilatore può assumere qualche altro valore più conveniente, ma potrebbe non trasformare la luna in formaggio verde, ecc. In secondo luogo, per quanto posso dire, questo vale solo per C ++. In C, l'intervallo di valori di un enumtipo è uguale all'intervallo di valori del tipo intero sottostante (che è definito dall'implementazione, quindi deve essere auto-coerente).
Jens Ayton,

1
Tuttavia, un'istanza di una variabile enum e una costante di enumerazione non devono necessariamente avere la stessa larghezza e lo stesso segno, ma sono entrambe impl.defined. Ma sono abbastanza sicuro che tutti gli enum in C siano valutati esattamente come il loro corrispondente numero intero, quindi non vi è alcun comportamento indefinito o addirittura non specificato lì.

2

Preferisco anche avere un default:in tutti i casi. Sono in ritardo alla festa come al solito, ma ... alcuni altri pensieri che non ho visto sopra:

  • L'avvertimento particolare (o errore se anche il lancio -Werror) viene -Wcovered-switch-default(da -Weverythingma non -Wall). Se la tua flessibilità morale ti consente di disattivare determinati avvisi (ad es., Accise alcune cose da -Wallo -Weverything), considera di lanciare -Wno-covered-switch-default(o -Wno-error=covered-switch-defaultquando usi -Werror) e in generale -Wno-...per altri avvisi che ritieni spiacevoli.
  • Per gcc(e il comportamento più generico a clang), consultare gccpagina di manuale per -Wswitch, -Wswitch-enum, -Wswitch-defaultper (diverso) comportamento in situazioni simili di tipi enumerati in istruzioni switch.
  • Anche questo concetto non mi piace, e non mi piace la sua formulazione; per me, le parole dall'avvertimento ("etichetta predefinita ... copre tutti ... valori") suggeriscono che il default:caso sarà sempre eseguito, come

    switch (foo) {
      case 1:
        do_something(); 
        //note the lack of break (etc.) here!
      default:
        do_default();
    }

    In prima lettura, questo è quello che pensavo ti stavi imbattendo: che il tuo default:caso sarebbe sempre stato eseguito perché non c'è break;o return;o simile. Questo concetto è simile (al mio orecchio) ad altri commenti in stile bambinaia (anche se occasionalmente utili) che emergono clang. Se foo == 1, entrambe le funzioni verranno eseguite; il tuo codice sopra ha questo comportamento. Vale a dire, non riuscire a scoppiare solo se si desidera continuare a eseguire il codice dai casi successivi! Questo sembra, tuttavia, non essere il tuo problema.

A rischio di essere pedanti, alcuni altri pensieri per completezza:

  • Penso tuttavia che questo comportamento sia (più) coerente con il controllo aggressivo dei tipi in altre lingue o compilatori. Se, come si ipotizzare, qualche reprobo non tenta di passare un into qualcosa a questa funzione, che è esplicitamente l'intenzione di consumare il proprio tipo specifico, il compilatore dovrebbe proteggere voi altrettanto bene in quella situazione con un avvertimento aggressivo o errore. MA no! (Cioè, sembra che almeno gcce clangnon fare il enumcontrollo del tipo, ma ho sentito che lo iccfa ). Dal momento che non si ottiene il tipo- sicurezza, è possibile ottenere valore- sicurezza come discusso sopra. Altrimenti, come suggerito in TFA, prendere in considerazione una structo qualcosa che può fornire sicurezza del tipo.
  • Un'altra soluzione alternativa potrebbe essere la creazione di un nuovo "valore" nel tuo enumtipo MaskValueIllegale non il supporto casenel tuo switch. Quello sarebbe mangiato dal default:(oltre a qualsiasi altro valore stravagante)

Lunga vita alla codifica difensiva!


1

Ecco un suggerimento alternativo:
l'OP sta cercando di proteggere dal caso in cui qualcuno passa in un luogo in intcui è previsto un enum . O, più probabilmente, in cui qualcuno ha collegato una vecchia libreria a un programma più recente utilizzando un'intestazione più recente con più casi.

Perché non cambiare l'interruttore per gestire il intcaso? L'aggiunta di un cast davanti al valore nell'opzione elimina l'avvertimento e fornisce anche un certo suggerimento sul perché esiste il valore predefinito.

void myFunction(testingMask theMask) {
    int maskValue = int(theMask);
    switch(maskValue) {
        case MaskValueUno: {} // deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
}

Trovo questo molto meno discutibile rispetto al assert()test di ciascuno dei possibili valori o persino al presupposto che l'intervallo di valori enum sia ben ordinato in modo che funzioni un test più semplice. Questo è solo un brutto modo di fare ciò che il default fa esattamente e magnificamente.


1
questo post è piuttosto difficile da leggere (wall of text). Ti dispiacerebbe modificarlo in una forma migliore?
moscerino
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.