Syntaxe valide mais sans valeur dans le boîtier de commutation?

207

À travers une petite faute de frappe, j'ai accidentellement trouvé cette construction:

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

Il semble que le printfhaut de la switchdéclaration soit valide, mais aussi complètement inaccessible.

J'ai obtenu une compilation propre, sans même un avertissement sur le code inaccessible, mais cela semble inutile.

Un compilateur doit-il signaler cela comme du code inaccessible?
Est-ce que cela sert à quelque chose que ce soit?

abelenky
la source
51
GCC a un drapeau spécial pour cela. C'est-Wswitch-unreachable
Eli Sadoff
57
"Est-ce que cela sert à quelque chose que ce soit?" Eh bien, vous pouvez gotoentrer et sortir de la partie autrement inaccessible, qui peut être utile pour divers hacks.
HolyBlackCat
12
@HolyBlackCat Ne serait-ce pas le cas pour tout le code inaccessible?
Eli Sadoff
28
@EliSadoff En effet. Je suppose que cela ne sert pas spécial fin. Je parie que c'est permis simplement parce qu'il n'y a aucune raison de l'interdire. Après tout, switchc'est juste un conditionnel gotoavec plusieurs étiquettes. Il y a plus ou moins les mêmes restrictions sur son corps que sur un bloc de code normal rempli d'étiquettes goto.
HolyBlackCat
16
Il convient de souligner que l'exemple de @MooingDuck est une variante de l'appareil de Duff ( en.wikipedia.org/wiki/Duff's_device )
Michael Anderson

Réponses:

226

Peut-être pas le plus utile, mais pas complètement inutile. Vous pouvez l'utiliser pour déclarer une variable locale disponible dans la switchportée.

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

Le standard ( N1579 6.8.4.2/7) a l'exemple suivant:

EXEMPLE Dans le fragment de programme artificiel

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

l'objet dont l'identifiant iexiste avec une durée de stockage automatique (dans le bloc) mais n'est jamais initialisé, et donc si l'expression de contrôle a une valeur non nulle, l'appel à la printffonction accède à une valeur indéterminée. De même, l'appel à la fonction fne peut pas être atteint.

PS BTW, l'exemple n'est pas du code C ++ valide. Dans ce cas ( N4140 6.7/3, c'est moi qui souligne):

Un programme qui passe de 90 d'un point où une variable avec une durée de stockage automatique n'est pas dans la portée à un point où elle est dans la portée est mal formé à moins que la variable ait un type scalaire , un type de classe avec un constructeur par défaut trivial et un destructeur trivial, une version qualifiée cv de l'un de ces types, ou un tableau de l'un des types précédents et est déclarée sans initialiseur (8.5).


90) Le passage de la condition d'une switchdéclaration à une étiquette de cas est considéré comme un saut à cet égard.

Donc, le remplacer int i = 4;par en int i;fait un C ++ valide.

AlexD
la source
3
"... mais n'est jamais initialisé ..." On dirait qu'il iest initialisé à 4, que me manque-t-il?
yano
7
Notez que si la variable est static, elle sera initialisée à zéro, il y a donc une utilisation sûre pour cela également.
Leushenko
23
@yano Nous sautons toujours par-dessus l' i = 4;initialisation, donc elle n'a jamais lieu.
AlexD
12
Hah bien sûr! ... tout le point de la question ... bon sang. Le désir est fort de supprimer cette bêtise
yano
1
Agréable! Parfois, j'avais besoin d'une variable temporaire dans un case, et je devais toujours utiliser des noms différents dans chacun caseou le définir en dehors du commutateur.
SJuan76
98

Est-ce que cela sert à quelque chose que ce soit?

Oui. Si au lieu d'une déclaration, vous mettez une déclaration avant la première étiquette, cela peut être parfaitement logique:

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

Les règles pour les déclarations et les instructions sont partagées pour les blocs en général, c'est donc la même règle qui autorise celle qui autorise également les instructions.


Il convient également de mentionner que si la première instruction est une construction de boucle, des étiquettes de casse peuvent apparaître dans le corps de la boucle:

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

Veuillez ne pas écrire de code comme celui-ci s'il existe un moyen plus lisible de l'écrire, mais il est parfaitement valide et l' f()appel est accessible.


la source
Le périphérique de @MatthieuM Duff a des étiquettes de casse à l'intérieur d'une boucle, mais commence par une étiquette de cas avant la boucle.
2
Je ne sais pas si je devrais voter pour l'exemple intéressant ou pour la folie totale d'écrire ceci dans un vrai programme :). Félicitations pour plonger dans l'abîme et revenir en un seul morceau.
Liviu T.19
@ChemicalEngineer: Si le code fait partie d'une boucle, comme c'est le cas dans Duff's Device, { /*code*/ switch(x) { } }peut sembler plus propre mais c'est aussi faux .
Ben Voigt
39

Il y a une utilisation célèbre de cet appareil appelé 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);
}

Ici, nous copions un tampon pointé par par fromun tampon pointé par to. Nous copions des countinstances de données.

L' do{}while()instruction commence avant la première caseétiquette et les caseétiquettes sont incorporées dans le do{}while().

Cela réduit le nombre de branches conditionnelles à la fin de la do{}while()boucle rencontrées d'environ un facteur 4 (dans cet exemple; la constante peut être ajustée à la valeur souhaitée).

Maintenant, les optimiseurs peuvent parfois le faire pour vous (surtout s'ils optimisent le streaming / les instructions vectorisées), mais sans optimisation guidée par le profil, ils ne peuvent pas savoir si vous vous attendez à ce que la boucle soit grande ou non.

En général, les déclarations de variables peuvent s'y produire et être utilisées dans tous les cas, mais être hors de portée une fois le commutateur terminé. (notez que toute initialisation sera ignorée)

De plus, un flux de contrôle qui n'est pas spécifique au commutateur peut vous amener dans cette section du bloc de commutateur, comme illustré ci-dessus, ou avec un goto.

Yakk - Adam Nevraumont
la source
2
Bien sûr, cela serait toujours possible sans autoriser les déclarations au-dessus du premier cas, car l'ordre do {et case 0:peu importe, les deux servent à placer une cible de saut sur le premier *to = *from++;.
Ben Voigt
1
@BenVoigt Je dirais que mettre le do {est plus lisible. Oui, argumenter sur la lisibilité de l'appareil de Duff est stupide et inutile et probablement un moyen simple de devenir fou.
Fund Monica's Lawsuit
1
@QPaysTaxes Vous devriez vérifier Simon Tatham Coroutines en C . Ou peut être pas.
Jonas Schäfer
@ JonasSchäfer De manière amusante, c'est essentiellement ce que les coroutines C ++ 20 vont faire pour vous.
Yakk - Adam Nevraumont
15

En supposant que vous utilisez gcc sous Linux, cela vous aurait donné un avertissement si vous utilisez 4.4 ou une version antérieure.

L'option -Wunreachable-code a été supprimée dans gcc 4.4 .

16 tonnes
la source
Avoir vécu le problème de première main aide toujours!
16 tonnes le
@JonathanLeffler: Le problème général des avertissements gcc étant susceptibles de l'ensemble particulier de passes d'optimisation sélectionné est toujours vrai, malheureusement, et fait une mauvaise expérience utilisateur. C'est vraiment ennuyeux d'avoir une version Debug propre suivie d'une version Release défaillante: /
Matthieu M.
@MatthieuM .: Il semblerait qu'un tel avertissement serait extrêmement facile à détecter dans les cas impliquant une analyse grammaticale plutôt que sémantique [par exemple, le code suit un "si" qui a des retours dans les deux branches], et faciliterait la répression de la "contrariété" avertissements. D'un autre côté, il y a des cas où il est utile d'avoir des erreurs ou des avertissements dans les versions de version qui ne sont pas dans les versions de débogage (si rien d'autre, dans les endroits où un hack qui est mis pour le débogage est censé être nettoyé pour Libération).
supercat
1
@MatthieuM.: Si une analyse sémantique significative était nécessaire pour découvrir qu'une certaine variable sera toujours fausse à un certain endroit du code, le code qui est conditionnel à la véracité de cette variable ne serait accessible que si une telle analyse était effectuée. D'un autre côté, je considérerais un avis qu'un tel code était inaccessible plutôt différemment d'un avertissement concernant un code syntaxiquement inaccessible, car il devrait être tout à fait normal que diverses conditions soient possibles avec certaines configurations de projet mais pas d'autres. Cela peut parfois être utile pour les programmeurs ...
supercat
1
... pour savoir pourquoi certaines configurations génèrent du code plus volumineux que d'autres [par exemple parce qu'un compilateur peut considérer une condition comme impossible dans une configuration mais pas dans une autre] mais cela ne signifie pas qu'il y a quelque chose de "mal" avec du code qui pourrait être optimisé dans cette mode avec certaines configurations.
supercat
11

Non seulement pour la déclaration de variables, mais aussi pour le saut avancé. Vous pouvez bien l'utiliser si et seulement si vous n'êtes pas sujet au code de spaghetti.

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

Impressions

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

Il convient de noter que le commutateur-cas est l'une des clauses de flux de contrôle les plus rapides. Il doit donc être très flexible pour le programmeur, ce qui implique parfois des cas comme celui-ci.

Sanchke Dellowar
la source
Et quelle est la différence entre nocase:et default:?
i486
@ i486 Lorsqu'il i=4ne se déclenche pas nocase.
Sanchke Dellowar
@SanchkeDellowar, c'est ce que je veux dire.
njzk2
Pourquoi diable ferait-on cela au lieu de simplement mettre le cas 1 avant le cas 0 et d'utiliser Fallthrough?
Jonas Schäfer
@JonasWielicki Dans cet objectif, vous pourriez le faire. Mais ce code n'est qu'une vitrine de ce qui peut être fait.
Sanchke Dellowar
11

Il convient de noter qu'il n'y a pratiquement aucune restriction structurelle sur le code dans l' switchénoncé ou sur l'emplacement des case *:étiquettes dans ce code *. Cela rend possible des astuces de programmation comme le périphérique de duff , dont une implémentation possible ressemble à ceci:

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

Vous voyez, le code entre le switch(n%8) {et lecase 7: étiquette est définitivement accessible ...


* Comme Supercat l'a heureusement souligné dans un commentaire : depuis C99, ni un gotoni une étiquette (que ce soit une case *:étiquette ou non) ne peut apparaître dans le cadre d'une déclaration contenant une déclaration VLA. Il n'est donc pas correct de dire qu'il n'y a pas de restrictions structurelles sur le placement des case *:étiquettes. Cependant, l'appareil de duff est antérieur à la norme C99, et il ne dépend pas de toute façon de VLA. Néanmoins, je me suis senti obligé d'insérer un "virtuellement" dans ma première phrase à cause de cela.

cmaster - réintégrer monica
la source
L'ajout de tableaux de longueur variable a conduit à l'imposition de restrictions structurelles qui leur sont liées.
supercat
@supercat Quel genre de restrictions?
cmaster - réintègre monica le
1
Ni une étiquette gotoni ne switch/case/defaultpeuvent apparaître dans la portée d'un objet ou d'un type à déclaration variable. Cela signifie effectivement que si un bloc contient des déclarations d'objets ou de types de tableaux de longueur variable, toutes les étiquettes doivent précéder ces déclarations. Il y a un peu de verbiage déroutant dans la norme qui suggère que dans certains cas, la portée d'une déclaration VLA s'étend à l'intégralité d'une instruction switch; voir stackoverflow.com/questions/41752072/… pour ma question à ce sujet.
supercat
@supercat: Vous venez de mal comprendre ce verbiage (qui, je suppose, est la raison pour laquelle vous avez supprimé votre question). Il impose une exigence sur la portée dans laquelle un VLA peut être défini. Il n'étend pas cette portée, il rend simplement certaines définitions VLA invalides.
Keith Thompson
@KeithThompson: Oui, je l'avais mal compris. L'utilisation étrange du présent dans la note de bas de page a rendu les choses déroutantes, et je pense que le concept aurait pu être mieux exprimé comme une interdiction: " portée de cette déclaration VLA ".
supercat
10

Vous avez obtenu votre réponse concernant l' option requisegcc -Wswitch-unreachable pour générer l'avertissement, cette réponse consiste à élaborer sur la partie convivialité / valeur .

Citant directement C11, chapitre §6.8.4.2, (c'est moi qui souligne )

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

l'objet dont l'identifiant iexiste avec une durée de stockage automatique (dans le bloc) mais n'est jamais initialisé , et donc si l'expression de contrôle a une valeur non nulle, l'appel à la printf fonction accède à une valeur indéterminée. De même, l'appel à la fonction fne peut pas être atteint.

Ce qui est très explicite. Vous pouvez l'utiliser pour définir une variable de portée locale qui n'est disponible que dans la switchportée de l' instruction.

Sourav Ghosh
la source
9

Il est possible d'implémenter une "boucle et demie" avec, bien que ce ne soit pas la meilleure façon de le faire:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));
celtschk
la source
@RichardII Est-ce un jeu de mots ou quoi? S'il vous plaît, expliquez.
Dancia
1
@Dancia Il dit que ce n'est clairement pas la meilleure façon de faire quelque chose comme ça, et "peut-être pas" est un euphémisme.
Fund Monica's Lawsuit