web-dev-qa-db-fra.com

Instruction Switch: le dernier cas doit-il être par défaut?

Considérez l'instruction switch suivante:

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

Ce code est compilé, mais est-il valide (= comportement défini) pour C90/C99? Je n'ai jamais vu de code où la casse par défaut n'est pas la dernière.

EDIT:
Comme Jon Cage et KillianDS écrivez: c'est code vraiment moche et déroutant et je suis bien au courant. Je ne m'intéresse qu'à la syntaxe générale (est-elle définie?) Et au résultat attendu.

162
tanascius

La norme C99 n’est pas explicite à ce sujet, mais si l’on tient compte de tous les faits, elle est parfaitement valable.

Une étiquette case et default équivaut à une étiquette goto. Voir 6.8.1 Déclarations étiquetées. 6.8.1.4 est particulièrement intéressant, ce qui active le périphérique de Duff déjà mentionné:

Toute déclaration peut être précédée d'un préfixe déclarant un identifiant sous forme de nom d'étiquette. Les étiquettes en elles-mêmes ne modifient pas le flux de contrôle, qui continue sans entrave.

Edit: Le code dans un commutateur n'a rien de spécial; c'est un bloc de code normal comme dans une instruction if-, avec des étiquettes de saut supplémentaires. Ceci explique le comportement de chute et pourquoi break est nécessaire.

Le 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'identificateur est i existe avec une durée de stockage automatique (dans le bloc) mais n'est jamais initialisé. Ainsi, si l'expression de contrôle a une valeur différente de zéro, l'appel de la fonction printf accédera à une valeur indéterminée. De même, l'appel de la fonction f est inaccessible.

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

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

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

6.8.4.2.5 Les promotions sur les entiers sont effectuées sur l'expression de contrôle. L'expression constante dans chaque étiquette de cas est convertie dans le 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 casse correspondant. Sinon, s'il existe une étiquette par défaut, le contrôle passe à l'instruction étiquetée. Si aucune expression constante de casse 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.

74
Secure

Les instructions case et la déclaration par défaut peuvent apparaître dans n'importe quel ordre de la déclaration switch. La clause default est une clause facultative mise en correspondance 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, ne pas dire cas 1, cas 3, cas 2/défaut) et que vos cas sont très longs, vous ne voudrez donc pas répéter tout le cas code en bas pour la valeur par défaut

78
Salil

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

Considérons 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 efficace que le cascadé if. Vous pouvez mettre default à la fin, mais cela n’a aucun sens car cela va attirer votre attention sur les cas d’erreur plutôt que sur les cas normaux (qui est le cas default.

En fait, ce n’est pas un si bon exemple, dans un sondage, vous savez combien d’événements peuvent se produire au maximum. Ce que je veux vraiment dire, c'est qu'il y a des cas avec un ensemble défini de valeurs d'entrée où il y a des "exceptions" et des cas normaux. S'il est préférable de placer les exceptions ou les cas normaux au premier plan, c'est une question de choix.

Dans le domaine des logiciels, je pense à un autre cas très habituel: les récursions avec certaines valeurs terminales. Si vous pouvez l'exprimer à l'aide d'un commutateur, default sera la valeur habituelle contenant 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 finales.

Une autre raison est que l'ordre des cas peut changer le comportement du code compilé, ce qui est important pour les performances. La plupart des compilateurs généreront le code Assembly compilé dans le même ordre que celui qui 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 videront les pipelines des processeurs. Vous l’entendez peut-être comme un prédicteur de branche qui exécute par défaut le premier cas figurant dans le commutateur. Si un cas est beaucoup plus courant que les autres, vous avez de très bonnes raisons de le définir comme premier cas.

Lire des commentaires, c’est la raison spécifique pour laquelle l’affiche originale a posé cette question après avoir lu Réorganisation du compilateur Intel Branch Loop à propos de l’optimisation du code.

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

45
kriss

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

15
Jens Gustedt

Il n'y a pas d'ordre défini dans une instruction switch. Vous pouvez considérer les cas comme une étiquette nommée, comme une étiquette goto. Contrairement à ce que les gens semblent penser ici, dans le cas de la valeur 2, l'étiquette par défaut n'est pas utilisée. Pour illustrer avec un exemple classique, voici le dispositif de Duff , qui est l’enfant affiche des extrêmes de switch/case en 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);
  }
}
8
Patrick Schlüter

Un scénario dans lequel il me semblerait approprié d’avoir une valeur par défaut située ailleurs que la fin d’une instruction case est celui d’une machine à états dans laquelle un état non valide doit réinitialiser la machine et procéder comme si c’était l’état initial. Par exemple:

commutateur 
 (widget_state) 
 {
 par défaut:/* Supprimer les rails - réinitialiser et continuer */
 widget_state = WIDGET_START; 
/* Chute dans */
 Cas WIDGET_START: 
 ... 
 Pause; 
 Cas WIDGET_WHATEVER: 
 ... 
 Pause ; 
} 

un autre arrangement, si un état non valide ne doit pas réinitialiser la machine mais doit être facilement identifiable en tant qu'état non valide:

commutateur 
 
 
 (widget_state) 
 {
 WIDGET_IDLE: 
 widget_ready = 0; 
 widget_hardware_off () ; 
 pause; 
 cas WIDGET_START: 
 ... 
 pause; 
 cas WIDGET_WHATEVER: 
 ... 
 break; 
 par défaut: 
 widget_state = WIDGET_INVALID_STATE; 
/* Chute dans */
 cas WIDGET_INVALID_STATE: 
 widget_ready = 0; 
 widget_hardware_off (); 
 ... faire tout ce qui est nécessaire pour établir une condition "sans danger" 
} 

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

7
supercat

Utiliser un autre exemple: Cela peut être utile si "default" est un cas inattendu et que vous souhaitez consigner l'erreur, mais également 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
  }
6
Brennan Vincent

Il existe des cas lorsque vous convertissez ENUM en chaîne ou que vous convertissez chaîne en enum au cas où vous écrivez/lisez dans un fichier.

Il est parfois nécessaire de définir une valeur par défaut pour couvrir les erreurs commises manuellement en modifiant des fichiers.

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

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

La condition "par défaut" peut être n'importe où dans le commutateur où une clause case peut exister. Ce n'est pas obligatoire d'être le dernier article. J'ai vu le code qui met la valeur par défaut comme premier article. Le "cas 2:" est exécuté normalement, même si la clause par défaut le précède.

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

  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
2
Scott Thomson

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

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

[edit] @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) si elle était écrite 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 pur d'un bon usage pour la suite:

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 une fonction propre:

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]; 
}
1
Jon Cage