Istruzione switch: l'impostazione predefinita deve essere l'ultimo caso?


178

Considera la seguente switchdichiarazione:

switch( value )
{
  case 1:
    return 1;
  default:
    value++;
    // fall-through
  case 2:
    return value * 2;
}

Questo codice viene compilato, ma è valido (= comportamento definito) per C90 / C99? Non ho mai visto il codice in cui il caso predefinito non è l'ultimo caso.

EDIT:
Come scrivono Jon Cage e KillianDS : questo è un codice davvero brutto e confuso e ne sono ben consapevole. Sono solo interessato alla sintassi generale (è definita?) E all'output previsto.


19
+1 Non ho mai preso in considerazione quel comportamento
Jamie Wong,

@ Péter Török: vuoi dire se value == 2 restituirà 6?
Alexandre C.

4
@Péter Török no, l'ordine non ha importanza: se il valore corrisponde alla costante in ogni caso l'etichetta, il controllo passerà a quell'istruzione seguendo l'etichetta, altrimenti il ​​controllo salterà all'istruzione che segue l'etichetta predefinita se presente.
Pete Kirkham,

11
@Jon Cage gotonon è malvagio. I seguaci del culto del carico sono! Non si può immaginare fino a che punto le persone possano evitare gotoperché è maledettamente così malvagia, facendo un vero pasticcio illeggibile del loro codice.
Patrick Schlüter,

3
Uso gotoprincipalmente per simulare qualcosa come una finallyclausola nelle funzioni, in cui risorse (file, memoria) devono essere rilasciate quando si interrompe e ripetendo per ogni errore un elenco di freee closenon aiuta per la leggibilità. C'è però un uso gotoche vorrei evitare ma che non posso, è quando voglio uscire da un ciclo e mi trovo all'interno di un switchcircuito.
Patrick Schlüter,

Risposte:


83

Lo standard C99 non è esplicito al riguardo, ma tenendo insieme tutti i fatti, è perfettamente valido.

A caseed defaultetichetta equivalgono ad gotoun'etichetta. Vedi 6.8.1 Dichiarazioni etichettate. Particolarmente interessante è il 6.8.1.4, che abilita il già citato dispositivo Duff:

Qualsiasi istruzione può essere preceduta da un prefisso che dichiara un identificatore come nome di un'etichetta. Le etichette in sé non alterano il flusso di controllo, che continua senza impedimenti attraverso di esse.

Modifica : il codice all'interno di un interruttore non è niente di speciale; è un normale blocco di codice come in uno ifstato, con etichette di salto aggiuntive. Questo spiega il comportamento fall-through e perchébreak è necessario.

6.8.4.2.7 fornisce anche un esempio:

switch (expr) 
{ 
    int i = 4; 
    f(i); 
case 0: 
    i=17; 
    /*falls through into default code */ 
default: 
    printf("%d\n", i); 
} 

Nel frammento di programma artificiale l'oggetto il cui identificatore è i esiste con durata di memorizzazione automatica (all'interno del blocco) ma non viene mai inizializzato, e quindi se l'espressione di controllo ha un valore diverso da zero, la chiamata alla funzione printf accederà a un valore indeterminato. Allo stesso modo, non è possibile raggiungere la chiamata alla funzione f.

Le costanti del caso devono essere univoche all'interno di un'istruzione switch:

6.8.4.2.3 L'espressione di ciascuna etichetta del caso deve essere un'espressione costante intera e nessuna delle due espressioni costanti del caso nella stessa istruzione switch deve avere lo stesso valore dopo la conversione. Potrebbe esserci al massimo un'etichetta predefinita in un'istruzione switch.

Tutti i casi vengono valutati, quindi passa all'etichetta predefinita, se fornita:

6.8.4.2.5 Le promozioni di numeri interi vengono eseguite sull'espressione di controllo. L'espressione costante in ogni etichetta del caso viene convertita nel tipo promosso dell'espressione di controllo. Se un valore convertito corrisponde a quello dell'espressione di controllo promossa, il controllo passa all'istruzione che segue l'etichetta del caso corrispondente. Altrimenti, se esiste un'etichetta predefinita, il controllo passa all'istruzione con etichetta. Se nessuna espressione di maiuscole / minuscole convertita corrisponde e non esiste un'etichetta predefinita, nessuna parte del corpo dell'interruttore viene eseguita.


6
@HeathHunnicutt Chiaramente non hai capito lo scopo dell'esempio. Il codice non è composto da questo poster, ma preso direttamente dallo standard C, come un'illustrazione di quanto siano strane le dichiarazioni degli switch e di come le cattive pratiche porteranno a bug. Se ti fossi preso la briga di leggere il testo sotto il codice, te ne renderesti conto.
Lundin,

2
+1 per compensare il downvote. Downvoting qualcuno dal citare lo standard C sembra abbastanza duro.
Lundin,

2
@Lundin Non sto votando al ribasso lo standard C, e non ho trascurato nulla come suggerisci. Ho votato contro la cattiva pedagogia dell'uso di un esempio cattivo e non necessario. In particolare, quell'esempio riguarda una situazione completamente diversa da quella richiesta. Potrei continuare, ma "grazie per il tuo feedback."
Heath Hunnicutt,

12
Intel ti dice di inserire prima il codice più frequente in un'istruzione switch in Branch and Loop Reorganization per prevenire errori di mercato . Sono qui perché ho un defaultcaso che domina altri casi di circa 100: 1 e non so se sia valido o indefinito per defaultil primo caso.
jww,

@jww Non sono sicuro di cosa intendi per Intel. Se intendi l'intelligenza, la chiamerò ipotesi. Ho avuto lo stesso pensiero, ma in seguito leggo che a differenza delle istruzioni if, le istruzioni switch sono accessi casuali. Quindi l'ultimo caso non è più lento da raggiungere rispetto al primo. Ciò si ottiene eseguendo l'hashing dei valori di caso costanti. Questo è il motivo per cui le istruzioni switch sono più veloci delle istruzioni if ​​quando i rami sono molti.

91

Le istruzioni case e l'istruzione default possono verificarsi in qualsiasi ordine nell'istruzione switch. La clausola predefinita è una clausola opzionale che viene abbinata se nessuna delle costanti nelle istruzioni case può essere abbinata.

Buon esempio :-

switch(5) {
  case 1:
    echo "1";
    break;
  case 2:
  default:
    echo "2, default";
    break;
  case 3;
    echo "3";
    break;
}


Outputs '2,default'

molto utile se vuoi che i tuoi casi siano presentati in un ordine logico nel codice (come in, senza dire il caso 1, il caso 3, il caso 2 / predefinito) e i tuoi casi sono molto lunghi, quindi non vuoi ripetere l'intero caso codice in basso per impostazione predefinita


7
Questo è esattamente lo scenario in cui di solito posiziono il default in un posto diverso dalla fine ... c'è un ordine logico per i casi espliciti (1, 2, 3) e voglio che il default si comporti esattamente come uno dei casi espliciti che non è l'ultimo.
ArtOfWarfare il

51

È valido e molto utile in alcuni casi.

Considera il seguente codice:

switch(poll(fds, 1, 1000000)){
   default:
    // here goes the normal case : some events occured
   break;
   case 0:
    // here goes the timeout case
   break;
   case -1:
     // some error occurred, you have to check errno
}

Il punto è che il codice sopra è più leggibile ed efficiente che in cascata if. Potresti metterlo defaultalla fine, ma è inutile in quanto focalizzerà la tua attenzione sui casi di errore anziché sui casi normali (che qui è il defaultcaso).

In realtà, non è un buon esempio, pollsapete quanti eventi possono verificarsi al massimo. Il mio vero punto è che ci sono casi con un set definito di valori di input in cui ci sono "eccezioni" e casi normali. Se è meglio mettere in primo piano eccezioni o casi normali è una questione di scelta.

Nel campo del software penso a un altro caso molto comune: ricorsioni con alcuni valori terminali. Se è possibile esprimerlo utilizzando un interruttore, defaultsarà il solito valore che contiene la chiamata ricorsiva e gli elementi distinti (singoli casi) i valori del terminale. Di solito non è necessario concentrarsi sui valori terminali.

Un altro motivo è che l'ordine dei casi può cambiare il comportamento del codice compilato e ciò che conta per le prestazioni. La maggior parte dei compilatori genererà il codice assembly compilato nello stesso ordine in cui appare il codice nello switch. Ciò rende il primo caso molto diverso dagli altri: tutti i casi tranne il primo comporteranno un salto e svuoteranno le condutture del processore. Potresti capirlo come il predittore di ramo che, per impostazione predefinita, esegue il primo caso che appare nello switch. Se un caso è molto più comune degli altri, allora hai ottime ragioni per metterlo come primo caso.

Leggere i commenti è il motivo specifico per cui il poster originale ha posto questa domanda dopo aver letto la riorganizzazione del compilatore Intel Branch Loop sull'ottimizzazione del codice.

Quindi diventerà un certo arbitrato tra leggibilità del codice e prestazioni del codice. Probabilmente è meglio inserire un commento per spiegare al futuro lettore perché un caso appare per primo.


6
+1 per dare un (buon) esempio senza il comportamento fallthrough.
KillianDS,

1
... pensandoci però, non sono convinto che il valore predefinito nella parte superiore sia buono perché pochissime persone lo cercheranno lì. Potrebbe essere meglio assegnare il ritorno a una variabile e gestire il successo in un lato di un if ed errori nell'altro lato con un'istruzione case.
Jon Cage,

@Jon: basta scriverlo. Aggiungete rumore sintattico senza alcun vantaggio di leggibilità. E, se il valore predefinito è al top, non c'è davvero bisogno di guardarlo, è davvero ovvio (potrebbe essere più complicato se lo metti nel mezzo).
Kriss,

A proposito, non mi piace molto la sintassi C switch / case. Preferirei di gran lunga essere in grado di mettere più etichette dopo un caso invece di essere obbligato a mettere diverse successive case. Ciò che è deprimente è che sembra zucchero sintassico e non romperà alcun codice esistente se supportato.
Kriss,

1
@kriss: ero quasi tentato di dire "Neanche io sono un programmatore di pitone!" :)
Andrew Grimm,

16

sì, questo è valido e in alcune circostanze è persino utile. Generalmente, se non ne hai bisogno, non farlo.


-1: Questo odora di male per me. Sarebbe meglio dividere il codice in una coppia di istruzioni switch.
Jon Cage,

25
@John Cage: mettermi un -1 qui è brutto. Non è colpa mia se questo è un codice valido.
Jens Gustedt,

solo curioso, vorrei sapere in quali circostanze è utile?
Salil,

1
Il -1 era mirato alla tua affermazione che fosse utile. Lo cambierò in +1 se puoi fornire un esempio valido per il backup del tuo reclamo.
Jon Cage,

4
A volte quando si passa a un errno che si ottiene in cambio da alcune funzioni di sistema. Supponiamo di avere un caso in cui sappiamo per sempre che dobbiamo fare un'uscita pulita, ma questa uscita pulita potrebbe richiedere alcune righe di codice che non vogliamo ripetere. Ma supponiamo anche di avere molti altri codici di errore esotici che non vogliamo gestire individualmente. Vorrei solo mettere un perror nel caso predefinito e lasciarlo scorrere nell'altro caso e uscire in modo pulito. Non dico che dovresti farlo in quel modo. È solo una questione di gusti.
Jens Gustedt,

8

Non esiste un ordine definito in un'istruzione switch. Puoi considerare i casi come qualcosa come un'etichetta denominata, come gotoun'etichetta. Contrariamente a quanto la gente sembra pensare qui, nel caso del valore 2 non viene saltata l'etichetta predefinita. Per illustrare con un esempio classico, ecco il dispositivo di Duff , che è il bambino poster degli estremi di switch/casein C.

send(to, from, count)
register short *to, *from;
register count;
{
  register n=(count+7)/8;
  switch(count%8){
    case 0: do{ *to = *from++;
    case 7:     *to = *from++;
    case 6:     *to = *from++;
    case 5:     *to = *from++;
    case 4:     *to = *from++;
    case 3:     *to = *from++;
    case 2:     *to = *from++;
    case 1:     *to = *from++;
            }while(--n>0);
  }
}

4
E per chiunque non abbia familiarità con il dispositivo di Duff questo codice è completamente illeggibile ...
KillianDS

7

Uno scenario in cui considererei appropriato disporre di un 'predefinito' situato in un punto diverso dalla fine di un'istruzione case è in una macchina a stati in cui uno stato non valido dovrebbe ripristinare la macchina e procedere come se fosse lo stato iniziale. Per esempio:

Interruttore (widget_state)
{
  impostazione predefinita: / * Abbassati dalle rotaie - ripristina e continua * /
    widget_state = WIDGET_START;
    /* Sfumare */
  case WIDGET_START:
    ...
    rompere;
  case WIDGET_WHATEVER:
    ...
    rompere;
}

un accordo alternativo, se uno stato non valido non deve ripristinare la macchina ma deve essere facilmente identificabile come stato non valido:

Interruttore (widget_state) { case WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); rompere; case WIDGET_START: ... rompere; case WIDGET_WHATEVER: ... rompere; predefinito: widget_state = WIDGET_INVALID_STATE; /* Sfumare */ case WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... fai tutto il necessario per stabilire una condizione "sicura" }

Il codice altrove può quindi verificare (widget_state == WIDGET_INVALID_STATE) e fornire qualsiasi comportamento di segnalazione errori o ripristino dello stato che sembra appropriato. Ad esempio, il codice a barre di stato potrebbe mostrare un'icona di errore e l'opzione di menu "Avvia widget" che è disabilitata nella maggior parte degli stati non inattivi potrebbe essere abilitata per WIDGET_INVALID_STATE e WIDGET_IDLE.


6

Comunica con un altro esempio: questo può essere utile se "default" è un caso imprevisto e vuoi registrare l'errore ma anche fare qualcosa di sensato. Esempio da parte del mio codice:

  switch (style)
  {
  default:
    MSPUB_DEBUG_MSG(("Couldn't match dash style, using solid line.\n"));
  case SOLID:
    return Dash(0, RECT_DOT);
  case DASH_SYS:
  {
    Dash ret(shapeLineWidth, dotStyle);
    ret.m_dots.push_back(Dot(1, 3 * shapeLineWidth));
    return ret;
  }
  // more cases follow
  }

5

Ci sono casi in cui si sta convertendo ENUM in una stringa o si converte una stringa in enum nel caso in cui si stia scrivendo / leggendo in / da un file.

A volte è necessario rendere predefinito uno dei valori per coprire gli errori commessi modificando manualmente i file.

switch(textureMode)
{
case ModeTiled:
default:
    // write to a file "tiled"
    break;

case ModeStretched:
    // write to a file "stretched"
    break;
}

2

La defaultcondizione può essere ovunque all'interno dell'interruttore che può esistere una clausola case. Non è necessario essere l'ultima clausola. Ho visto il codice che inserisce il valore predefinito come prima clausola. Il case 2:Viene eseguito normalmente, anche se la clausola di default è al di sopra di esso.

Come test, ho inserito il codice di esempio in una funzione, chiamato test(int value){}ed eseguito:

  printf("0=%d\n", test(0));
  printf("1=%d\n", test(1));
  printf("2=%d\n", test(2));
  printf("3=%d\n", test(3));
  printf("4=%d\n", test(4));

L'output è:

0=2
1=1
2=4
3=8
4=10

1

È valido, ma piuttosto cattivo. Vorrei suggerire che in genere è male consentire fallimenti poiché può portare a un codice di spaghetti molto disordinato.

È quasi certamente meglio suddividere questi casi in più istruzioni switch o funzioni più piccole.

[modifica] @Tristopia: il tuo esempio:

Example from UCS-2 to UTF-8 conversion 

r is the destination array, 
wc is the input wchar_t  

switch(utf8_length) 
{ 
    /* Note: code falls through cases! */ 
    case 3: r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
    case 2: r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 
    case 1: r[0] = wc;
}

sarebbe più chiaro sulla sua intenzione (penso) se fosse scritto in questo modo:

if( utf8_length >= 1 )
{
    r[0] = wc;

    if( utf8_length >= 2 )
    {
        r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 

        if( utf8_length == 3 )
        {
            r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
        }
    }
}   

[edit2] @Tristopia: il tuo secondo esempio è probabilmente l'esempio più pulito di un buon uso per il follow-through:

for(i=0; s[i]; i++)
{
    switch(s[i])
    {
    case '"': 
    case '\'': 
    case '\\': 
        d[dlen++] = '\\'; 
        /* fall through */ 
    default: 
        d[dlen++] = s[i]; 
    } 
}

..ma personalmente dividerei il riconoscimento dei commenti nella sua funzione:

bool isComment(char charInQuestion)
{   
    bool charIsComment = false;
    switch(charInQuestion)
    {
    case '"': 
    case '\'': 
    case '\\': 
        charIsComment = true; 
    default: 
        charIsComment = false; 
    } 
    return charIsComment;
}

for(i=0; s[i]; i++)
{
    if( isComment(s[i]) )
    {
        d[dlen++] = '\\'; 
    }
    d[dlen++] = s[i]; 
}

2
Ci sono casi in cui fall è davvero, davvero una buona idea.
Patrick Schlüter,

Esempio di conversione da UCS-2 a UTF-8 rè l'array di destinazione, wcè l' wchar_t interruttore di input (utf8_length) {/ * Nota: il codice cade nei casi! * / caso 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; caso 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; caso 1: r [0] = wc; }
Patrick Schlüter,

Eccone un'altra, una routine di copia di stringa con carattere in fuga: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter,

Sì, ma questa routine è uno dei nostri hotspot, questo è stato il modo più veloce e portatile (non faremo assemblaggio) per implementarlo. Ha solo 1 test per qualsiasi lunghezza UTF, il tuo ne ha 2 o anche 3. Inoltre, non me ne sono inventato, l'ho preso da BSD.
Patrick Schlüter,

1
Sì, ci sono stati, soprattutto nelle conversioni in bulgaro e greco (su Solaris SPARC) e nel testo con il nostro markup interno (che è 3 byte UTF8). Ammesso, in toto non è molto ed è diventato irrilevante dal nostro ultimo aggiornamento hardware, ma al momento in cui è stato scritto ha fatto la differenza.
Patrick Schlüter,
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.