Switch-Anweisung: Muss standardmäßig der letzte Fall sein?


178

Betrachten Sie die folgende switchAussage:

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

Dieser Code wird kompiliert, aber ist er für C90 / C99 gültig (= definiertes Verhalten)? Ich habe noch nie Code gesehen, bei dem der Standardfall nicht der letzte ist.

EDIT:
Wie Jon Cage und KillianDS schreiben: Das ist wirklich hässlicher und verwirrender Code und ich bin mir dessen sehr bewusst. Ich interessiere mich nur für die allgemeine Syntax (ist sie definiert?) Und die erwartete Ausgabe.


19
+1 Ich habe dieses Verhalten noch nie in Betracht gezogen
Jamie Wong

@ Péter Török: du meinst wenn Wert == 2 wird es 6 zurückgeben?
Alexandre C.

4
@ Péter Török nein, die Reihenfolge spielt keine Rolle - wenn der Wert in jedem Fall mit der Konstante übereinstimmt, springt die Steuerung zu dieser Anweisung nach der Bezeichnung, andernfalls springt die Steuerung zu der Anweisung nach der Standardbezeichnung, falls vorhanden.
Pete Kirkham

11
@ Jon Cage gotoist nicht böse. Frachtkult Anhänger sind! Sie können sich nicht vorstellen, welche Extreme Menschen vermeiden können, gotoweil es angeblich so böse ist und ihren Code wirklich unleserlich macht.
Patrick Schlüter

3
Ich gotosimuliere hauptsächlich so etwas wie eine finallyKlausel in Funktionen, in denen Ressourcen (Dateien, Speicher) beim Stoppen freigegeben werden müssen, und wiederhole für jeden Fehlerfall eine Liste von freeund closehilft nicht bei der Lesbarkeit. Es gibt zwar eine Verwendung goto, die ich vermeiden möchte, aber nicht kann, wenn ich aus einer Schleife ausbrechen möchte und mich in einer switchin dieser Schleife befinde.
Patrick Schlüter

Antworten:


83

Der C99-Standard ist diesbezüglich nicht explizit, aber wenn man alle Fakten zusammenfasst, ist er vollkommen gültig.

A caseund defaultLabel entsprechen einem gotoLabel. Siehe 6.8.1 Beschriftete Anweisungen. Besonders interessant ist 6.8.1.4, das das bereits erwähnte Duff's Device ermöglicht:

Jeder Anweisung kann ein Präfix vorangestellt werden, das einen Bezeichner als Bezeichnungsnamen deklariert. Etiketten an sich verändern nicht den Kontrollfluss, der über sie hinweg ungehindert weitergeht.

Bearbeiten : Der Code innerhalb eines Schalters ist nichts Besonderes; Es ist ein normaler Codeblock wie in einer ifAnweisung mit zusätzlichen Sprungbezeichnungen. Dies erklärt das Durchfallverhalten und warum breakes notwendig ist.

6.8.4.2.7 gibt sogar ein Beispiel:

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

In dem künstlichen Programmfragment existiert das Objekt, dessen Bezeichner i ist, mit automatischer Speicherdauer (innerhalb des Blocks), wird jedoch nie initialisiert. Wenn der steuernde Ausdruck einen Wert ungleich Null hat, greift der Aufruf der Funktion printf auf einen unbestimmten Wert zu. Ebenso kann der Aufruf der Funktion f nicht erreicht werden.

Die case-Konstanten müssen innerhalb einer switch-Anweisung eindeutig sein:

6.8.4.2.3 Der Ausdruck jeder Fallbezeichnung muss ein ganzzahliger Konstantenausdruck sein, und keine zwei der Fallkonstantenausdrücke in derselben switch-Anweisung dürfen nach der Konvertierung denselben Wert haben. Eine switch-Anweisung darf höchstens eine Standardbezeichnung enthalten.

Alle Fälle werden ausgewertet und springen dann zur Standardbezeichnung, falls angegeben:

6.8.4.2.5 Die ganzzahligen Beförderungen werden für den steuernden Ausdruck durchgeführt. Der jeweils konstante Ausdruck wird in den heraufgestuften Typ des steuernden Ausdrucks konvertiert. Wenn ein konvertierter Wert mit dem des heraufgestuften Steuerungsausdrucks übereinstimmt, springt die Steuerung zu der Anweisung, die der übereinstimmenden Fallbezeichnung folgt. Wenn andernfalls eine Standardbezeichnung vorhanden ist, springt die Steuerung zur beschrifteten Anweisung. Wenn kein Ausdruck der konvertierten Groß- / Kleinschreibung übereinstimmt und keine Standardbezeichnung vorhanden ist, wird kein Teil des Schalterkörpers ausgeführt.


6
@HeathHunnicutt Du hast den Zweck des Beispiels offensichtlich nicht verstanden. Der Code besteht nicht aus diesem Poster, sondern stammt direkt aus dem C-Standard, um zu veranschaulichen, wie seltsam switch-Anweisungen sind und wie schlechte Praktiken zu Fehlern führen. Wenn Sie sich die Mühe gemacht hätten, den Text unter dem Code zu lesen, würden Sie genau das merken.
Lundin

2
+1, um die Abwertung zu kompensieren. Jemanden vom Zitieren des C-Standards abzulehnen, scheint ziemlich hart zu sein.
Lundin

2
@Lundin Ich stimme den C-Standard nicht ab und habe nichts übersehen, wie Sie vorschlagen. Ich habe die schlechte Pädagogik, ein schlechtes und unnötiges Beispiel zu verwenden, abgelehnt. Dieses Beispiel bezieht sich insbesondere auf eine völlig andere Situation als die, nach der gefragt wurde. Ich könnte weitermachen, aber "Danke für Ihr Feedback."
Heath Hunnicutt

12
Intel fordert Sie auf, den häufigsten Code zuerst in einer switch-Anweisung bei Branch and Loop Reorganization zu platzieren, um Fehlvorhersagen zu vermeiden . Ich bin hier, weil ich einen defaultFall habe, der andere Fälle um etwa 100: 1 dominiert, und ich weiß nicht, ob er gültig oder undefiniert ist, um defaultden ersten Fall zu machen .
JWW

@jww Ich bin mir nicht sicher, was du mit Intel meinst. Wenn Sie Intelligenz meinen, werde ich es Hypothese nennen. Ich hatte das gleiche Denken, aber später heißt es beim Lesen, dass switch-Anweisungen im Gegensatz zu if-Anweisungen Direktzugriff sind. Der letzte Fall ist also nicht langsamer zu erreichen als der erste. Dies wird durch Hashing der konstanten Fallwerte erreicht. Aus diesem Grund sind switch-Anweisungen schneller als if-Anweisungen, wenn die Zweige viel sind.

91

Die case-Anweisungen und die default-Anweisung können in beliebiger Reihenfolge in der switch-Anweisung vorkommen. Die Standardklausel ist eine optionale Klausel, die übereinstimmt, wenn keine der Konstanten in den case-Anweisungen übereinstimmen kann.

Gutes Beispiel :-

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


Outputs '2,default'

Sehr nützlich, wenn Sie möchten, dass Ihre Fälle im Code in einer logischen Reihenfolge dargestellt werden (wie in Fall 1, Fall 3, Fall 2 / Standard) und Ihre Fälle sehr lang sind, sodass Sie nicht den gesamten Fall wiederholen möchten Code unten für die Standardeinstellung


7
Dies ist genau das Szenario, in dem ich die Standardeinstellung normalerweise an einem anderen Ort als dem Ende platziere. Die expliziten Fälle (1, 2, 3) haben eine logische Reihenfolge, und ich möchte, dass sich die Standardeinstellung genauso verhält wie einer der expliziten Fälle ist nicht der letzte.
ArtOfWarfare

51

Es ist gültig und in einigen Fällen sehr nützlich.

Betrachten Sie den folgenden Code:

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
}

Der Punkt ist, dass der obige Code lesbarer und effizienter als kaskadiert ist if. Sie könnten defaultam Ende setzen, aber es ist sinnlos, da es Ihre Aufmerksamkeit auf Fehlerfälle anstatt auf normale Fälle lenkt (was hier der defaultFall ist).

Eigentlich ist es kein so gutes Beispiel, pollwenn Sie wissen, wie viele Ereignisse höchstens auftreten können. Mein eigentlicher Punkt ist , dass es sind Fälle mit einem definierten Satz von Eingabewerten , wo es ‚Ausnahmen‘ und normale Fälle. Ob es besser ist, Ausnahmen oder Normalfälle in den Vordergrund zu stellen, ist eine Frage der Wahl.

Im Softwarebereich denke ich an einen anderen sehr üblichen Fall: Rekursionen mit einigen Endwerten. Wenn Sie es mit einem Schalter ausdrücken können,default wird der übliche Wert, der rekursiven Aufruf und unterscheidbare Elemente (Einzelfälle) enthält, die Terminalwerte sein. Es ist normalerweise nicht erforderlich, sich auf Endwerte zu konzentrieren.

Ein weiterer Grund ist, dass die Reihenfolge der Fälle das Verhalten des kompilierten Codes ändern kann und dies für die Leistung von Bedeutung ist. Die meisten Compiler generieren kompilierten Assemblycode in derselben Reihenfolge, in der der Code im Switch angezeigt wird. Das unterscheidet den ersten Fall sehr von den anderen: Alle Fälle außer dem ersten beinhalten einen Sprung und leeren Prozessor-Pipelines. Möglicherweise verstehen Sie es wie einen Verzweigungsprädiktor, der standardmäßig den ersten angezeigten Fall im Switch ausführt. Wenn ein Fall viel häufiger ist als die anderen, dann haben Sie sehr gute Gründe, ihn als ersten Fall zu bezeichnen.

Das Lesen von Kommentaren ist der spezifische Grund, warum das Originalplakat diese Frage gestellt hat, nachdem die Reorganisation des Intel Compilers Branch Loop zur Codeoptimierung gelesen wurde .

Dann wird es zu einer Schlichtung zwischen Codelesbarkeit und Codeleistung kommen. Wahrscheinlich ist es besser, einen Kommentar abzugeben, um dem zukünftigen Leser zu erklären, warum ein Fall zuerst auftritt.


6
+1 für ein (gutes) Beispiel ohne das Durchfallverhalten.
KillianDS

1
... wenn ich darüber nachdenke, bin ich nicht davon überzeugt, dass es gut ist, die Standardeinstellung an der Spitze zu haben, da nur sehr wenige Leute dort danach suchen würden. Es ist möglicherweise besser, die Rückgabe einer Variablen zuzuweisen und den Erfolg auf einer Seite eines if und Fehler auf der anderen Seite mit einer case-Anweisung zu behandeln.
Jon Cage

@ Jon: schreib es einfach. Sie fügen syntaktisches Rauschen ohne Lesbarkeitsvorteile hinzu. Und wenn die Standardeinstellung oben ist, ist es wirklich nicht nötig, sie anzusehen, es ist wirklich offensichtlich (es könnte schwieriger sein, wenn Sie sie in die Mitte stellen).
Kriss

Übrigens mag ich die C-Switch / Case-Syntax nicht wirklich. Ich würde es sehr bevorzugen, mehrere Etiketten nach einem Fall anbringen zu können, anstatt mehrere aufeinanderfolgende Etiketten anbringen zu müssen case. Was bedrückend ist, ist, dass es genauso aussieht wie syntaxischer Zucker und keinen vorhandenen Code beschädigt, wenn es unterstützt wird.
Kriss

1
@kriss: Ich war halb versucht zu sagen "Ich bin auch kein Python-Programmierer!" :)
Andrew Grimm

16

Ja, das ist gültig und unter Umständen sogar nützlich. Wenn Sie es nicht brauchen, tun Sie es im Allgemeinen nicht.


-1: Das riecht für mich nach Bösem. Es wäre besser, den Code in zwei switch-Anweisungen aufzuteilen.
Jon Cage

25
@ John Cage: Es ist böse, mir hier eine -1 zu geben. Es ist nicht meine Schuld, dass dies ein gültiger Code ist.
Jens Gustedt

nur neugierig, ich würde gerne wissen, unter welchen Umständen es nützlich ist?
Salil

1
Das -1 zielte darauf ab, dass Sie behaupten, es sei nützlich. Ich werde es in +1 ändern, wenn Sie ein gültiges Beispiel zur Sicherung Ihres Anspruchs angeben können.
Jon Cage

4
Manchmal beim Umschalten auf einen Fehler, den wir als Gegenleistung von einer Systemfunktion erhalten haben. Angenommen, wir haben einen Fall, in dem wir endgültig wissen, dass wir einen sauberen Exit durchführen müssen, aber für diesen sauberen Exit sind möglicherweise einige Codierungszeilen erforderlich, die wir nicht wiederholen möchten. Angenommen, wir haben auch viele andere exotische Fehlercodes, die wir nicht einzeln behandeln möchten. Ich würde in Betracht ziehen, nur einen Perror in den Standardfall zu setzen und ihn in den anderen Fall durchlaufen zu lassen und sauber zu beenden. Ich sage nicht, dass du es so machen sollst. Es ist nur eine Frage des Geschmacks.
Jens Gustedt

8

In einer switch-Anweisung ist keine Reihenfolge definiert. Sie können die Fälle als so etwas wie ein benanntes Etikett betrachten, wie ein gotoEtikett. Im Gegensatz zu dem, was die Leute hier zu denken scheinen, wird im Fall von Wert 2 nicht auf die Standardbezeichnung gesprungen. Um dies mit einem klassischen Beispiel zu veranschaulichen, hier Duffs Gerät , das das Aushängeschild der Extreme von 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
Und für alle, die mit Duffs Gerät nicht vertraut sind, ist dieser Code völlig unlesbar ...
KillianDS

7

Ein Szenario, in dem ich es für angemessen halte, einen 'Standard' an einem anderen Ort als dem Ende einer case-Anweisung zu haben, befindet sich in einer Zustandsmaschine, in der ein ungültiger Zustand die Maschine zurücksetzen und so vorgehen sollte, als wäre es der Anfangszustand. Beispielsweise:

switch (widget_state)
{
  Standard: / * Von den Schienen gefallen - zurücksetzen und fortfahren * /
    widget_state = WIDGET_START;
    /* Durchfallen */
  case WIDGET_START:
    ...
    brechen;
  case WIDGET_WHATEVER:
    ...
    brechen;
}}

eine alternative Anordnung, wenn ein ungültiger Zustand die Maschine nicht zurücksetzen sollte, aber leicht als ungültiger Zustand erkennbar sein sollte:

switch (widget_state) { case WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); brechen; case WIDGET_START: ... brechen; case WIDGET_WHATEVER: ... brechen; Standard: widget_state = WIDGET_INVALID_STATE; /* Durchfallen */ case WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... alles Notwendige tun, um einen "sicheren" Zustand herzustellen }}

Code an anderer Stelle kann dann nach (widget_state == WIDGET_INVALID_STATE) suchen und das für die Fehlerberichterstattung oder das Zurücksetzen des Status geeignete Verhalten bereitstellen. Beispielsweise könnte der Status-Barcode ein Fehlersymbol anzeigen, und die Menüoption "Widget starten", die in den meisten nicht inaktiven Zuständen deaktiviert ist, könnte sowohl für WIDGET_INVALID_STATE als auch für WIDGET_IDLE aktiviert werden.


6

Ein anderes Beispiel: Dies kann nützlich sein, wenn "Standard" ein unerwarteter Fall ist und Sie den Fehler protokollieren möchten, aber auch etwas Sinnvolles tun möchten. Beispiel aus einem meiner eigenen Codes:

  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

Es gibt Fälle, in denen Sie ENUM in eine Zeichenfolge oder Zeichenfolge in enum konvertieren, wenn Sie in eine Datei schreiben / lesen.

Manchmal müssen Sie einen der Werte als Standard festlegen, um Fehler abzudecken, die beim manuellen Bearbeiten von Dateien auftreten.

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

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

2

Die defaultBedingung kann überall innerhalb des Schalters sein, dass eine case-Klausel existieren kann. Es muss nicht die letzte Klausel sein. Ich habe Code gesehen, der den Standard als erste Klausel setzt. Das case 2:wird normal ausgeführt, obwohl die Standardklausel darüber steht.

Als Test habe ich den Beispielcode in eine Funktion eingefügt, aufgerufen test(int value){}und ausgeführt:

  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));

Die Ausgabe ist:

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

1

Es ist gültig, aber ziemlich böse. Ich würde vorschlagen, dass es im Allgemeinen schlecht ist, Durchbrüche zuzulassen, da dies zu einem sehr unordentlichen Spaghetti-Code führen kann.

Es ist mit ziemlicher Sicherheit besser, diese Fälle in mehrere switch-Anweisungen oder kleinere Funktionen aufzuteilen.

[Bearbeiten] @Tristopia: Ihr Beispiel:

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;
}

wäre klarer in Bezug auf seine Absicht (glaube ich), wenn es so geschrieben wäre:

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: Ihr zweites Beispiel ist wahrscheinlich das sauberste Beispiel für eine gute Verwendung für die Nachverfolgung:

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

..aber persönlich würde ich die Kommentarerkennung in eine eigene Funktion aufteilen:

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
Es gibt Fälle, in denen ein Durchfallen wirklich eine gute Idee ist.
Patrick Schlüter

Beispiel für die Konvertierung von UCS-2 nach UTF-8 rist das Zielarray , wcder Eingangsschalter wchar_t(utf8_length) {/ * Hinweis: Code fällt durch Fälle! * / Fall 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; Fall 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; Fall 1: r [0] = wc; }
Patrick Schlüter

Hier noch eine, eine String-Kopierroutine mit entkommendem Charakter: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter

Ja, aber diese Routine ist einer unserer Hotspots. Dies war der schnellste und tragbarste Weg, sie zu implementieren (wir werden keine Montage durchführen). Es hat nur 1 Test für jede UTF-Länge, Ihre hat 2 oder sogar 3. Außerdem habe ich es mir nicht ausgedacht, ich habe es von BSD genommen.
Patrick Schlüter

1
Ja, insbesondere bei Konvertierungen in Bulgarisch und Griechisch (unter Solaris SPARC) und Text mit unserem internen Markup (3 Byte UTF8). Zugegeben, insgesamt ist es nicht viel und seit unserem letzten Hardware-Update irrelevant geworden, aber zu dem Zeitpunkt, als es geschrieben wurde, hat es einen Unterschied gemacht.
Patrick Schlüter
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.