Gültige, aber wertlose Syntax im Switch-Case?

207

Durch einen kleinen Tippfehler habe ich versehentlich dieses Konstrukt gefunden:

int main(void) {
    char foo = 'c';

    switch(foo)
    {
        printf("Cant Touch This\n");   // This line is Unreachable

        case 'a': printf("A\n"); break;
        case 'b': printf("B\n"); break;
        case 'c': printf("C\n"); break;
        case 'd': printf("D\n"); break;
    }

    return 0;
}

Es scheint, dass die printf oben in der switchAussage gültige, aber auch völlig unerreichbare ist.

Ich habe eine saubere Kompilierung erhalten, ohne auch nur eine Warnung vor nicht erreichbarem Code, aber das scheint sinnlos.

Sollte ein Compiler dies als nicht erreichbaren Code kennzeichnen?
Hat dies überhaupt einen Zweck?

abelenky
quelle
51
GCC hat dafür eine spezielle Flagge. Es ist-Wswitch-unreachable
Eli Sadoff
57
"Dient dies überhaupt einem Zweck?" Nun, Sie können gotoin den ansonsten nicht erreichbaren Teil ein- und aussteigen, was für verschiedene Hacks nützlich sein kann.
HolyBlackCat
12
@HolyBlackCat Wäre das nicht für alle nicht erreichbaren Codes so?
Eli Sadoff
28
@ EliSadoff In der Tat. Ich denke, es dient keinem besonderen Zweck. Ich wette, es ist erlaubt, nur weil es keinen Grund gibt, es zu verbieten. Immerhin switchist nur eine Bedingung gotomit mehreren Beschriftungen. Es gibt mehr oder weniger dieselben Einschränkungen für den Körper wie für einen normalen Codeblock, der mit goto-Labels gefüllt ist.
HolyBlackCat
16
Es sei darauf hingewiesen, dass das Beispiel von @MooingDuck eine Variante auf Duffs Gerät ist ( en.wikipedia.org/wiki/Duff's_device )
Michael Anderson

Antworten:

226

Vielleicht nicht das Nützlichste, aber nicht völlig wertlos. Sie können es verwenden, um eine lokale Variable zu deklarieren, die innerhalb des switchGültigkeitsbereichs verfügbar ist .

switch (foo)
{
    int i;
case 0:
    i = 0;
    //....
case 1:
    i = 1;
    //....
}

Der Standard ( N1579 6.8.4.2/7) hat das folgende Beispiel:

BEISPIEL Im künstlichen Programmfragment

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

Das Objekt, dessen Kennung imit automatischer Speicherdauer (innerhalb des Blocks) vorhanden ist, jedoch niemals initialisiert wird. Wenn der steuernde Ausdruck einen Wert ungleich Null hat, greift der Aufruf der printfFunktion auf einen unbestimmten Wert zu. Ebenso kann der Aufruf der Funktion fnicht erreicht werden.

PS Übrigens ist das Beispiel kein gültiger C ++ - Code. In diesem Fall ( N4140 6.7/3Schwerpunkt meiner):

Ein Programm , das Sprung 90 von einem Punkt , wo eine Variable mit einer Dauer dynamischer Speichers ist in ihrem Umfang nicht bis zu einem Punkt , wo sie in ihrem Umfang ist schlecht gebildet , wenn die variable skalare Typen hat , Klassentyp mit einer trivialen Standardkonstruktors und einem trivialen destructor, eine cv-qualifizierte Version eines dieser Typen oder ein Array eines der vorhergehenden Typen und wird ohne Initialisierer deklariert (8.5).


90) Die Übertragung von der Bedingung einer switchAussage auf ein Falletikett wird in dieser Hinsicht als Sprung angesehen.

Das Ersetzen int i = 4;durch int i;macht es also zu einem gültigen C ++.

AlexD
quelle
3
"... wird aber nie initialisiert ..." Sieht so aus, als wäre ies auf 4 initialisiert. Was fehlt mir?
Yano
7
Beachten Sie, dass die Variable staticbei Null auf Null initialisiert wird, sodass auch hierfür eine sichere Verwendung besteht.
Leushenko
23
@yano Wir springen immer über die i = 4;Initialisierung, also findet sie nie statt.
AlexD
12
Hah natürlich! ... der springende Punkt der Frage ... meine Güte. Der Wunsch ist stark, diese Dummheit zu löschen
Yano
1
Nett! Manchmal brauchte ich eine temporäre Variable innerhalb von a caseund musste immer unterschiedliche Namen verwenden caseoder sie außerhalb des Schalters definieren.
SJuan76
98

Hat dies überhaupt einen Zweck?

Ja. Wenn Sie anstelle einer Aussage eine Erklärung vor das erste Etikett setzen, kann dies durchaus Sinn machen:

switch (a) {
  int i;
case 0:
  i = f(); g(); h(i);
  break;
case 1:
  i = g(); f(); h(i);
  break;
}

Die Regeln für Deklarationen und Anweisungen werden im Allgemeinen für Blöcke gemeinsam genutzt. Daher ist es dieselbe Regel, die dies zulässt, dass auch Anweisungen dort zulässig sind.


Erwähnenswert ist auch, dass, wenn die erste Anweisung ein Schleifenkonstrukt ist, Fallbezeichnungen im Schleifenkörper erscheinen können:

switch (i) {
  for (;;) {
    f();
  case 1:
    g();
  case 2:
    if (h()) break;
  }
}

Bitte schreiben Sie keinen Code wie diesen, wenn es eine besser lesbare Schreibweise gibt, diese jedoch vollkommen gültig ist und der f()Anruf erreichbar ist.


quelle
@MatthieuM Duffs Gerät hat zwar Fallbezeichnungen in einer Schleife, beginnt jedoch mit einer Fallbezeichnung vor der Schleife.
2
Ich bin mir nicht sicher, ob ich für das interessante Beispiel stimmen oder für den Wahnsinn, dies in einem echten Programm zu schreiben, ablehnen soll :). Herzlichen Glückwunsch zum Eintauchen in den Abgrund und zur Rückkehr in einem Stück.
Liviu T.
@ChemicalEngineer: Wenn der Code Teil einer Schleife ist, wie er in Duffs Gerät enthalten ist, { /*code*/ switch(x) { } }sieht er möglicherweise sauberer aus, ist aber auch falsch .
Ben Voigt
39

Es gibt eine berühmte Verwendung dieses Duff's Device .

int n = (count+3)/4;
switch (count % 4) {
  do {
    case 0: *to = *from++;
    case 3: *to = *from++;
    case 2: *to = *from++;
    case 1: *to = *from++;
  } while (--n > 0);
}

Hier kopieren wir einen Puffer, auf den von gezeigt wird, fromin einen Puffer, auf den von gezeigt wird to. Wir kopieren countDateninstanzen.

Die do{}while()Anweisung beginnt vor dem ersten caseLabel und die caseLabels sind in das eingebettet do{}while().

Dies reduziert die Anzahl der bedingten Verzweigungen am Ende der do{}while()Schleife um ungefähr den Faktor 4 (in diesem Beispiel kann die Konstante auf den gewünschten Wert angepasst werden).

Jetzt können Optimierer dies manchmal für Sie tun (insbesondere wenn sie Streaming- / vektorisierte Anweisungen optimieren), aber ohne profilgesteuerte Optimierung können sie nicht wissen, ob Sie erwarten, dass die Schleife groß ist oder nicht.

Im Allgemeinen können dort Variablendeklarationen auftreten, die in jedem Fall verwendet werden, aber nach dem Ende des Schalters außerhalb des Gültigkeitsbereichs liegen. (Beachten Sie, dass jede Initialisierung übersprungen wird.)

Darüber hinaus können Sie durch einen nicht schalterspezifischen Steuerungsfluss in diesen Abschnitt des Schaltblocks gelangen, wie oben dargestellt, oder mit einem goto.

Yakk - Adam Nevraumont
quelle
2
Dies wäre natürlich immer noch möglich, ohne Aussagen über dem ersten Fall zuzulassen, da die Reihenfolge do {und case 0:egal sind, beide dazu dienen, ein Sprungziel auf den ersten zu setzen *to = *from++;.
Ben Voigt
1
@ BenVoigt Ich würde argumentieren, dass das Setzen der do {ist besser lesbar. Ja, über die Lesbarkeit von Duffs Gerät zu streiten ist dumm und sinnlos und wahrscheinlich ein einfacher Weg, verrückt zu werden.
Fund Monica Klage
1
@QPaysTaxes Sie sollten sich Simon Tathams Coroutines in C ansehen . Oder vielleicht nicht.
Jonas Schäfer
@ JonasSchäfer Amüsanterweise ist das im Grunde das, was C ++ 20 Coroutinen für Sie tun werden.
Yakk - Adam Nevraumont
15

Angenommen, Sie verwenden gcc unter Linux, hätte es Sie gewarnt, wenn Sie 4.4 oder eine frühere Version verwenden.

Die Option -Wunreachable-Code wurde ab gcc 4.4 entfernt .

16 Tonnen
quelle
Das Problem aus erster Hand erlebt zu haben, hilft immer!
16 Tonnen
@ JonathanLeffler: Das allgemeine Problem, dass gcc-Warnungen für die ausgewählten Optimierungsdurchläufe anfällig sind, ist leider immer noch zutreffend und führt zu einer schlechten Benutzererfahrung. Es ist wirklich ärgerlich, einen sauberen Debug-Build zu haben, gefolgt von einem fehlgeschlagenen Release-Build: /
Matthieu M.
@MatthieuM.: Es scheint, dass eine solche Warnung in Fällen, die eher eine grammatikalische als eine semantische Analyse beinhalten, extrem leicht zu erkennen ist [z. B. folgt der Code einem "Wenn", das in beiden Zweigen zurückkehrt], und es einfacher macht, "Ärger" zu unterdrücken. Warnungen. Auf der anderen Seite gibt es einige Fälle, in denen es nützlich ist, Fehler oder Warnungen in Release-Builds zu haben, die nicht in Debug-Builds enthalten sind (nicht zuletzt an Orten, an denen ein Hack, der zum Debuggen eingesetzt wird, bereinigt werden soll Freisetzung).
Supercat
1
@MatthieuM.: Wenn eine signifikante semantische Analyse erforderlich wäre, um festzustellen, dass eine bestimmte Variable an einer bestimmten Stelle im Code immer falsch ist, wird Code, der davon abhängig ist, dass diese Variable wahr ist, nur dann als nicht erreichbar befunden, wenn eine solche Analyse durchgeführt würde. Andererseits würde ich einen Hinweis in Betracht ziehen, dass ein solcher Code nicht erreichbar war, ganz anders als eine Warnung vor syntaktisch nicht erreichbarem Code, da es völlig normal sein sollte, dass bei einigen Projektkonfigurationen verschiedene Bedingungen möglich sind, bei anderen jedoch nicht. Es kann manchmal für Programmierer hilfreich sein ...
Supercat
1
... um zu wissen, warum einige Konfigurationen größeren Code generieren als andere [z. B. weil ein Compiler eine Bedingung in einer Konfiguration als unmöglich, aber nicht in einer anderen als unmöglich ansehen könnte], aber das bedeutet nicht, dass mit Code "falsch" ist, der optimiert werden könnte diese Mode mit einigen Konfigurationen.
Supercat
11

Nicht nur für die Variablendeklaration, sondern auch für fortgeschrittenes Springen. Sie können es genau dann gut verwenden, wenn Sie nicht anfällig für Spaghetti-Code sind.

int main()
{
    int i = 1;
    switch(i)
    {
        nocase:
        printf("no case\n");

        case 0: printf("0\n"); break;
        case 1: printf("1\n"); goto nocase;
    }
    return 0;
}

Druckt

1
no case
0 /* Notice how "0" prints even though i = 1 */

Es ist zu beachten, dass der Schaltfall eine der schnellsten Kontrollflussklauseln ist. Daher muss es für den Programmierer sehr flexibel sein, was manchmal solche Fälle betrifft.

Sanchke Dellowar
quelle
Und was ist der Unterschied zwischen nocase:und default:?
i486
@ i486 Wenn i=4es nicht ausgelöst wird nocase.
Sanchke Dellowar
@SanchkeDellowar das meine ich.
NJZK2
Warum zum Teufel sollte man das tun, anstatt einfach Fall 1 vor Fall 0 zu setzen und Fallthrough zu verwenden?
Jonas Schäfer
@ JonasWielicki In diesem Ziel könnten Sie das tun. Dieser Code ist jedoch nur eine Vitrine dessen, was getan werden kann.
Sanchke Dellowar
11

Es ist zu beachten, dass der Code in der switchAnweisung oder die case *:Position der Beschriftungen in diesem Code * praktisch keine strukturellen Einschränkungen aufweist . Dies ermöglicht Programmiertricks wie das Gerät von duff , von denen eine mögliche Implementierung folgendermaßen aussieht:

int n = ...;
int iterations = n/8;
switch(n%8) {
    while(iterations--) {
        sum += *ptr++;
        case 7: sum += *ptr++;
        case 6: sum += *ptr++;
        case 5: sum += *ptr++;
        case 4: sum += *ptr++;
        case 3: sum += *ptr++;
        case 2: sum += *ptr++;
        case 1: sum += *ptr++;
        case 0: ;
    }
}

Sie sehen, der Code zwischen dem switch(n%8) {und dem case 7:Etikett ist definitiv erreichbar ...


* Wie Supercat dankbar in einem Kommentar hervorhob : Seit C99 darf weder ein gotonoch ein Etikett (sei es ein case *:Etikett oder nicht) im Rahmen einer Erklärung erscheinen, die eine VLA-Erklärung enthält. Es ist also nicht richtig zu sagen, dass es keine strukturellen Einschränkungen bei der Platzierung der case *:Etiketten gibt. Das Gerät von duff ist jedoch älter als der C99-Standard und hängt sowieso nicht von den VLAs ab. Trotzdem fühlte ich mich gezwungen, aus diesem Grund ein "virtuelles" in meinen ersten Satz einzufügen.

cmaster - Monica wieder einsetzen
quelle
Das Hinzufügen von Arrays variabler Länge führte dazu, dass damit verbundene strukturelle Einschränkungen auferlegt wurden.
Supercat
@supercat Welche Einschränkungen?
cmaster
1
Weder ein gotonoch eine switch/case/defaultBezeichnung dürfen im Bereich eines variabel deklarierten Objekts oder Typs erscheinen. Dies bedeutet effektiv, dass Beschriftungen vor diesen Deklarationen stehen müssen, wenn ein Block Deklarationen von Array-Objekten oder -Typen variabler Länge enthält. Der Standard enthält eine verwirrende Redewendung, die darauf hindeutet, dass sich der Geltungsbereich einer VLA-Deklaration in einigen Fällen auf die Gesamtheit einer switch-Anweisung erstreckt. Siehe stackoverflow.com/questions/41752072/… für meine Frage dazu.
Supercat
@supercat: Du hast diese Redewendung einfach falsch verstanden (was vermutlich der Grund ist, warum du deine Frage gelöscht hast). Es stellt eine Anforderung an den Umfang, in dem eine VLA definiert werden kann. Dieser Bereich wird nicht erweitert, sondern nur bestimmte VLA-Definitionen werden ungültig.
Keith Thompson
@ KeithThompson: Ja, ich hatte es falsch verstanden. Die seltsame Verwendung der Gegenwart in der Fußnote machte die Dinge verwirrend, und ich denke, das Konzept hätte besser als Verbot ausgedrückt werden können: "Eine switch-Anweisung, deren Hauptteil eine VLA-Erklärung enthält, darf keine switch- oder case-Labels in der enthalten Geltungsbereich dieser VLA-Erklärung ".
Supercat
10

Sie haben Ihre Antwort in Bezug auf die erforderliche gccOption -Wswitch-unreachable zum Generieren der Warnung erhalten. Diese Antwort dient dazu, den Teil Benutzerfreundlichkeit / Wertigkeit zu erläutern .

Zitat direkt aus C11Kapitel §6.8.4.2 ( Hervorhebung von mir )

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

Das Objekt, dessen Kennung imit automatischer Speicherdauer (innerhalb des Blocks) vorhanden ist, jedoch niemals initialisiert wird. Wenn der steuernde Ausdruck einen Wert ungleich Null hat, greift der Aufruf der printf Funktion auf einen unbestimmten Wert zu. Ebenso kann der Aufruf der Funktion fnicht erreicht werden.

Welches ist sehr selbsterklärend. Mit dieser Option können Sie eine Variable mit lokalem Gültigkeitsbereich definieren, die nur innerhalb des switchAnweisungsbereichs verfügbar ist .

Sourav Ghosh
quelle
9

Es ist möglich, eine "anderthalb-Schleife" damit zu implementieren, obwohl dies möglicherweise nicht der beste Weg ist, dies zu tun:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));
Celtschk
quelle
@ Richard II Ist es ein Wortspiel oder was? Bitte erkläre.
Dancia
1
@Dancia Er sagt, dass dies ganz klar nicht der beste Weg ist, so etwas zu tun, und "vielleicht nicht" ist etwas untertrieben.
Fund Monica Klage