Instruction Switch: la valeur par défaut doit-elle être le dernier cas?

178

Considérez la switchdéclaration suivante :

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

Ce code se compile, mais est-il valide (= comportement défini) pour C90 / C99? Je n'ai jamais vu de code où le cas par défaut n'est pas le dernier cas.

EDIT:
Comme l' écrivent Jon Cage et KillianDS : c'est un code vraiment laid et déroutant et j'en suis bien conscient. Je suis juste intéressé par la syntaxe générale (est-elle définie?) Et le résultat attendu.

Tanascius
la source
19
+1 Jamais même envisagé ce comportement
Jamie Wong
@ Péter Török: vous voulez dire que si valeur == 2, il retournera 6?
Alexandre C.
4
@ Péter Török non, l'ordre n'a pas d'importance - si la valeur correspond à la constante dans tous les cas, le contrôle passera à cette instruction après le libellé, sinon le contrôle passera à l'instruction suivant le libellé par défaut s'il est présent.
Pete Kirkham
11
@Jon Cage goton'est pas mal. Les adeptes du culte du fret le sont! Vous ne pouvez pas imaginer les extrêmes que les gens peuvent éviter gotoparce que c'est prétendument si maléfique, ce qui crée un véritable gâchis illisible de leur code.
Patrick Schlüter
3
J'utilise gotoprincipalement pour simuler quelque chose comme une finallyclause dans les fonctions, où les ressources (fichiers, mémoire) doivent être libérées lors de l'arrêt, et répéter pour chaque cas d'erreur une liste de freeet closen'aide pas à la lisibilité. Il y a bien une utilisation gotoque j'aimerais éviter mais que je ne peux pas, c'est quand je veux sortir d'une boucle et que je suis dans switchcette boucle.
Patrick Schlüter

Réponses:

83

La norme C99 n'est pas explicite à ce sujet, mais en prenant tous les faits ensemble, elle est parfaitement valable.

A caseet defaultlabel sont équivalents à un gotolabel. Voir 6.8.1 Déclarations étiquetées. Particulièrement intéressant est 6.8.1.4, qui active le dispositif de Duff déjà mentionné:

Toute instruction peut être précédée d'un préfixe qui déclare un identifiant comme nom d'étiquette. Les étiquettes en elles-mêmes ne modifient pas le flux de contrôle, qui se poursuit sans entrave à travers elles.

Edit : Le code dans un commutateur n'a rien de spécial; c'est un bloc de code normal comme dans une ifinstruction-, avec des étiquettes de saut supplémentaires. Cela explique le comportement fall-through et pourquoi breakest nécessaire.

6.8.4.2.7 donne même un exemple:

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

Dans le fragment de programme artificiel, l'objet dont l'identifiant est i existe 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 différente de zéro, l'appel à la fonction printf accédera à une valeur indéterminée. De même, l'appel à la fonction f ne peut pas être atteint.

Les constantes de cas doivent être uniques dans une instruction switch:

6.8.4.2.3 L'expression de chaque étiquette de cas doit être une expression de constante entière et aucune des deux expressions de constante de cas dans la même instruction de commutation ne doit avoir la même valeur après conversion. Il peut y avoir au plus une étiquette par défaut dans une instruction switch.

Tous les cas sont évalués, puis il passe à l'étiquette par défaut, si elle est donnée:

6.8.4.2.5 Les promotions d'entiers sont effectuées sur l'expression de contrôle. L'expression constante dans chaque étiquette de cas est convertie en type promu de l'expression de contrôle. Si une valeur convertie correspond à celle de l'expression de contrôle promue, le contrôle passe à l'instruction suivant le libellé de cas correspondant. Sinon, s'il existe une étiquette par défaut, le contrôle passe à l'instruction étiquetée. Si aucune expression de constante de cas convertie ne correspond et qu'il n'y a pas d'étiquette par défaut, aucune partie du corps du commutateur n'est exécutée.

Sécurise
la source
6
@HeathHunnicutt Vous n'avez clairement pas compris le but de l'exemple. Le code n'est pas constitué par cette affiche, mais tiré directement du standard C, pour illustrer à quel point les instructions de commutation sont étranges et à quel point les mauvaises pratiques entraîneront des bogues. Si vous aviez pris la peine de lire le texte sous le code, vous vous en rendriez compte.
Lundin
2
+1 pour compenser le vote défavorable. Refuser quelqu'un de citer la norme C semble assez sévère.
Lundin
2
@Lundin Je ne vote pas contre la norme C et je n'ai rien oublié comme vous le suggérez. J'ai voté à la baisse pour la mauvaise pédagogie consistant à utiliser un exemple mauvais et inutile. En particulier, cet exemple concerne une situation totalement différente de celle qui a été posée. Je pourrais continuer, mais "merci pour vos commentaires".
Heath Hunnicutt
12
Intel vous demande de placer le code le plus fréquent en premier dans une instruction de commutation lors de la réorganisation des branchements et des boucles pour éviter les erreurs de prédiction . Je suis ici parce que j'ai un defaultcas dominant les autres cas d'environ 100: 1, et je ne sais pas s'il est valide ou indéfini pour faire defaultle premier cas.
jww
@jww Je ne suis pas sûr de ce que vous entendez par Intel. Si vous voulez dire l'intelligence, je l'appellerai hypothèse. J'avais la même pensée, mais une lecture ultérieure indique que contrairement aux instructions if, les instructions switch sont à accès aléatoire. Le dernier cas n'est donc pas plus lent à atteindre que le premier. Ceci est accompli en hachant les valeurs de cas constantes. C'est pourquoi les instructions switch sont plus rapides que les instructions if lorsque les branches sont nombreuses.
91

Les instructions case et l'instruction par défaut peuvent se produire dans n'importe quel ordre dans l'instruction switch. La clause par défaut est une clause facultative qui correspond si aucune des constantes des instructions case ne peut être mise en correspondance.

Bon exemple :-

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


Outputs '2,default'

très utile si vous voulez que vos cas soient présentés dans un ordre logique dans le code (comme dans, sans dire cas 1, cas 3, cas 2 / par défaut) et que vos cas sont très longs donc vous ne voulez pas répéter le cas entier code en bas pour la valeur par défaut

Salil
la source
7
C'est exactement le scénario où je place habituellement la valeur par défaut ailleurs qu'à la fin ... il y a un ordre logique pour les cas explicites (1, 2, 3) et je veux que la valeur par défaut se comporte exactement de la même manière que l'un des cas explicites qui n'est pas le dernier.
ArtOfWarfare
51

C'est valable et très utile dans certains cas.

Considérez le code suivant:

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
}

Le fait est que le code ci-dessus est plus lisible et plus efficace qu'en cascade if. Vous pourriez mettre defaultà la fin, mais cela ne sert à rien car cela concentrera votre attention sur les cas d'erreur au lieu des cas normaux (ce qui est le defaultcas ici ).

En fait, ce n'est pas un si bon exemple, car pollvous savez combien d'événements peuvent se produire au maximum. Mon vrai point est qu'il existe des cas avec un ensemble défini de valeurs d'entrée où il y a des «exceptions» et des cas normaux. S'il vaut mieux placer des exceptions ou des cas normaux au premier plan, c'est une question de choix.

Dans le domaine du logiciel, je pense à un autre cas très courant: les récursions avec certaines valeurs terminales. Si vous pouvez l'exprimer à l'aide d'un commutateur, defaultsera la valeur habituelle qui contient l'appel récursif et les éléments distingués (cas individuels) les valeurs du terminal. Il n'est généralement pas nécessaire de se concentrer sur les valeurs terminales.

Une autre raison est que l'ordre des cas peut changer le comportement du code compilé, ce qui compte pour les performances. La plupart des compilateurs génèrent du code d'assemblage compilé dans le même ordre que le code apparaît dans le commutateur. Cela rend le premier cas très différent des autres: tous les cas sauf le premier impliqueront un saut et cela videra les pipelines du processeur. Vous pouvez le comprendre comme un prédicteur de branche exécutant par défaut le premier cas apparaissant dans le commutateur. Si un cas est beaucoup plus courant que les autres, vous avez de très bonnes raisons de le mettre comme premier cas.

La lecture des commentaires est la raison spécifique pour laquelle l'affiche originale a posé cette question après avoir lu la réorganisation de la boucle de branche du compilateur Intel sur l'optimisation du code.

Ensuite, cela deviendra un arbitrage entre la lisibilité du code et les performances du code. Il vaut probablement mieux mettre un commentaire pour expliquer au futur lecteur pourquoi un cas apparaît en premier.

Kriss
la source
6
+1 pour donner un (bon) exemple sans le comportement de chute.
KillianDS
1
... en y réfléchissant cependant, je ne suis pas convaincu qu'avoir la valeur par défaut en haut soit une bonne chose car très peu de gens le chercheraient là-bas. Il peut être préférable d'affecter le retour à une variable et de gérer le succès d'un côté d'un if et les erreurs de l'autre côté avec une instruction case.
Jon Cage
@Jon: écrivez-le. Vous ajoutez du bruit syntaxique sans aucun avantage de lisibilité. Et, si le défaut est en haut, il n'y a vraiment pas besoin de le regarder, c'est vraiment évident (cela pourrait être plus délicat si vous le mettez au milieu).
kriss
Au fait, je n'aime pas vraiment la syntaxe C switch / case. Je préférerais de loin pouvoir mettre plusieurs étiquettes après un cas au lieu d'être obligé d'en mettre plusieurs successivement case. Ce qui est déprimant, c'est qu'il ressemble à du sucre syntaxique et ne cassera aucun code existant s'il est pris en charge.
kriss
1
@kriss: J'étais à moitié tenté de dire "Je ne suis pas non plus un programmeur python!" :)
Andrew Grimm
16

oui, c'est valable, et dans certaines circonstances c'est même utile. Généralement, si vous n'en avez pas besoin, ne le faites pas.

Jens Gustedt
la source
-1: Cela sent le mal pour moi. Il serait préférable de diviser le code en une paire d'instructions switch.
Jon Cage
25
@John Cage: me mettre un -1 ici est méchant. Ce n'est pas ma faute si ce code est valide.
Jens Gustedt
juste curieux, je voudrais savoir dans quelles circonstances il est utile?
Salil
1
Le -1 visait à ce que votre affirmation soit utile. Je le changerai en +1 si vous pouvez fournir un exemple valide pour sauvegarder votre réclamation.
Jon Cage
4
Parfois, lors de la commutation pour un errno que nous avons obtenu en retour d'une fonction système. Supposons que nous ayons un cas où nous savons pour de bon que nous devons effectuer une sortie propre, mais cette sortie propre peut nécessiter des lignes de codage que nous ne voulons pas répéter. Mais supposons également que nous ayons aussi beaucoup d'autres codes d'erreur exotiques que nous ne voulons pas gérer individuellement. J'envisagerais simplement de mettre un perror dans le cas par défaut et de le laisser passer à l'autre cas et de sortir proprement. Je ne dis pas que vous devriez le faire comme ça. C'est juste une question de goût.
Jens Gustedt
8

Il n'y a pas d'ordre défini dans une instruction switch. Vous pouvez considérer les cas comme quelque chose comme une étiquette nommée, comme une gotoétiquette. Contrairement à ce que les gens semblent penser ici, dans le cas de la valeur 2, l'étiquette par défaut n'est pas sautée. Pour illustrer avec un exemple classique, voici le dispositif de Duff , qui est l'affiche des extrêmes de switch/caseC.

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);
  }
}
Patrick Schlüter
la source
4
Et pour quiconque ne connaît pas l'appareil de Duff, ce code est complètement illisible ...
KillianDS
7

Un scénario dans lequel je considérerais qu'il est approprié d'avoir un «défaut» situé ailleurs que la fin d'une instruction de cas est dans une machine à états où un état invalide devrait réinitialiser la machine et procéder comme s'il s'agissait de l'état initial. Par exemple:

commutateur (widget_state)
{
  par défaut: / * Fell off the rails - reset and continue * /
    widget_state = WIDGET_START;
    /* Tomber dans */
  case WIDGET_START:
    ...
    Pause;
  case WIDGET_WHATEVER:
    ...
    Pause;
}

une disposition alternative, si un état invalide ne doit pas réinitialiser la machine mais doit être facilement identifiable comme un état invalide:

commutateur (widget_state) { case WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); Pause; case WIDGET_START: ... Pause; case WIDGET_WHATEVER: ... Pause; défaut: widget_state = WIDGET_INVALID_STATE; /* Tomber dans */ case WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... faire tout ce qui est nécessaire pour établir une condition "sûre" }

Le code ailleurs peut alors vérifier (widget_state == WIDGET_INVALID_STATE) et fournir tout comportement de rapport d'erreur ou de réinitialisation d'état qui semble approprié. Par exemple, le code à barres d'état peut afficher une icône d'erreur et l'option de menu "Démarrer le widget", qui est désactivée dans la plupart des états non inactifs, peut être activée pour WIDGET_INVALID_STATE ainsi que WIDGET_IDLE.

supercat
la source
6

Ajout d'un autre exemple: cela peut être utile si "default" est un cas inattendu et que vous voulez enregistrer l'erreur mais aussi faire quelque chose de sensé. Exemple de certains de mes propres 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
  }
Brennan Vincent
la source
5

Il y a des cas où vous convertissez ENUM en chaîne ou convertissez une chaîne en enum dans le cas où vous écrivez / lisez dans / à partir d'un fichier.

Vous devez parfois définir l'une des valeurs par défaut pour couvrir les erreurs commises en modifiant manuellement les fichiers.

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

case ModeStretched:
    // write to a file "stretched"
    break;
}
Predrag Manojlovic
la source
2

La defaultcondition peut être n'importe où dans le commutateur qu'une clause case peut exister. Il n'est pas nécessaire que ce soit la dernière clause. J'ai vu du code qui met la valeur par défaut comme première clause. Le case 2:est exécuté normalement, même si la clause par défaut est au-dessus.

En guise de test, j'ai mis l'exemple de code dans une fonction, appelée test(int value){}et exécutée:

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

La sortie est:

0=2
1=1
2=4
3=8
4=10
Scott Thomson
la source
1

C'est valable, mais plutôt méchant. Je dirais qu'il est généralement mauvais d'autoriser les retombées car cela peut conduire à un code spaghetti très désordonné.

Il est presque certainement préférable de diviser ces cas en plusieurs instructions de commutation ou des fonctions plus petites.

[modifier] @Tristopia: Votre exemple:

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

serait plus clair quant à son intention (je pense) s'il était écrit comme ceci:

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: Votre deuxième exemple est probablement l'exemple le plus clair d'une bonne utilisation pour le suivi:

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

..mais personnellement, je diviserais la reconnaissance des commentaires en sa propre fonction:

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]; 
}
Jon Cage
la source
2
Il y a des cas où la chute est vraiment, vraiment une bonne idée.
Patrick Schlüter
Exemple de conversion UCS-2 à UTF-8 rest le tableau de destination, wcest le wchar_t commutateur d' entrée (utf8_length) {/ * Remarque: le code passe par les cas! * / cas 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; cas 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; cas 1: r [0] = wc; }
Patrick Schlüter
En voici une autre, une routine de copie de chaîne avec échappement de caractère: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter
Oui, mais cette routine est l'un de nos hotspots, c'était le moyen le plus rapide et portable (nous ne ferons pas d'assemblage) de l'implémenter. Il n'a qu'un seul test pour n'importe quelle longueur UTF, le vôtre en a 2 ou même 3. En plus, je ne l'ai pas proposé, je l'ai pris sur BSD.
Patrick Schlüter
1
Oui, il y en avait, en particulier dans les conversions en bulgare et grec (sur Solaris SPARC) et du texte avec notre balisage interne (qui est de 3 octets UTF8). Certes, dans l'ensemble, ce n'est pas grand-chose et cela n'a plus d'importance depuis notre dernière mise à jour matérielle, mais au moment où elle a été écrite, cela a fait une différence.
Patrick Schlüter