1091 votes

Pourquoi les variables ne peuvent-elles pas être déclarées dans une instruction switch ?

Je me suis toujours posé la question suivante : pourquoi ne peut-on pas déclarer des variables après une étiquette case dans une instruction switch ? En C++, vous pouvez déclarer des variables à peu près n'importe où (et les déclarer à proximité de leur première utilisation est évidemment une bonne chose), mais ce qui suit ne fonctionne toujours pas :

switch (val)  
{  
case VAL:  
  // This won't work
  int newVal = 42;  
  break;
case ANOTHER_VAL:  
  ...
  break;
}  

Ce qui précède me donne l'erreur suivante (MSC) :

L'initialisation de 'newVal' est ignorée par l'étiquette 'case'.

Cela semble être une limitation dans d'autres langues également. Pourquoi est-ce un tel problème ?

10 votes

Pour une explication basée sur la grammaire C BNF, voir stackoverflow.com/questions/1180550/weird-switch-error-in-obj-c/

0 votes

Voici une très bonne lecture à propos des déclarations d'interrupteurs et des étiquettes (ABC :) en général.

5 votes

Je dirais "Pourquoi les variables ne peuvent-elles pas être initialisées dans une instruction switch plutôt que déclarées", puisque la simple déclaration de la variable ne me donne qu'un avertissement dans MSVC.

1302voto

Thomas Points 6891

Case les déclarations sont seulement étiquettes . Cela signifie que le compilateur l'interprétera comme un saut directement à l'étiquette. En C++, le problème ici est celui de la portée. Vos accolades définissent la portée comme étant tout ce qui se trouve à l'intérieur de l'élément switch déclaration. Cela signifie que vous vous retrouvez avec une portée où un saut sera effectué plus loin dans le code en sautant l'initialisation.

La manière correcte de gérer cette situation est de définir une portée spécifique à cet élément case et définissez votre variable dans celle-ci :

switch (val)
{   
case VAL:  
{
  // This will work
  int newVal = 42;  
  break;
}
case ANOTHER_VAL:  
...
break;
}

95 votes

Par rapport à l'ouverture d'une nouvelle portée - favoriser la lisibilité et la cohérence du code. Dans le passé, vous auriez pu obtenir automatiquement un cadre de pile "supplémentaire", mais aujourd'hui cela ne devrait pas être le cas pour tout compilateur optimisant décent.

11 votes

Je suis d'accord avec Jeff - il est trop facile de "présumer" de la portée lorsqu'on lit une instruction de commutation à cause du style d'indentation que la plupart des gens utilisent. Mon propre style consiste à toujours ouvrir une nouvelle portée pour chaque cas/défaut s'il fait plus d'une ligne.

40 votes

Workmad3 - Pouvez-vous me trouver un compilateur C++ qui génère un nouveau cadre de pile si vous ne déclarez pas de nouvelles variables ? Vous m'avez inquiété brièvement, mais aucun des compilateurs G++ 3.1, Visual C++ 7 ou Intel C++ 8 ne génère de code pour les nouveaux périmètres où vous ne déclarez aucune variable.

435voto

AndreyT Points 139512

Cette question a été initialement étiquetée comme c y c++ en même temps. Le code original est en effet invalide à la fois en C et en C++, mais pour des raisons complètement différentes et non liées.

  • En C++, ce code n'est pas valable parce que l'élément case ANOTHER_VAL: saute dans la portée de la variable newVal en contournant son initialisation. Les sauts qui contournent l'initialisation d'objets automatiques sont illégaux en C++. Cet aspect de la question est correctement traité par la plupart des réponses.

  • Cependant, en langage C, le contournement de l'initialisation des variables n'est pas une erreur. Sauter dans la portée d'une variable par-dessus son initialisation est légal en C. Cela signifie simplement que la variable est laissée non initialisée. Le code original ne compile pas en C pour une raison complètement différente. Étiquette case VAL: dans le code original est attaché à la déclaration de la variable newVal . En langage C, les déclarations ne sont pas des instructions. Elles ne peuvent pas être étiquetées. Et c'est ce qui provoque l'erreur lorsque ce code est interprété comme du code C.

      switch (val)  
      {  
      case VAL:             /* <- C error is here */
        int newVal = 42;  
        break;
      case ANOTHER_VAL:     /* <- C++ error is here */
        ...
        break;
      }

    Ajout d'un supplément {} Ce bloc résout les problèmes du C++ et du C, même si ces problèmes sont très différents. Du côté du C++, il restreint la portée de la fonction newVal en s'assurant que case ANOTHER_VAL: ne saute plus dans cette portée, ce qui élimine le problème du C++. Du côté du C, cet élément supplémentaire {} introduit une déclaration composée, rendant ainsi le case VAL: à appliquer à une déclaration, ce qui élimine le problème du C.

  • Dans le cas de C, le problème peut être facilement résolu sans le {} . Il suffit d'ajouter une déclaration vide après le case VAL: et le code deviendra valide

      switch (val)  
      {  
      case VAL:;            /* Now it works in C! */
        int newVal = 42;  
        break;
      case ANOTHER_VAL:  
        ...
        break;
      }

    Notez que même si elle est maintenant valide du point de vue du C, elle reste invalide du point de vue du C++.

  • Symétriquement, dans le cas du C++, le problème peut être résolu facilement sans le {} . Il suffit de supprimer l'initialisateur de la déclaration de la variable et le code deviendra valide.

      switch (val)  
      {  
      case VAL: 
        int newVal;
        newVal = 42;  
        break;
      case ANOTHER_VAL:     /* Now it works in C++! */
        ...
        break;
      }

    Notez que même si elle est maintenant valide du point de vue du C++, elle reste invalide du point de vue du C.

6 votes

@AnT : Je comprends pourquoi celui qui corrige le C++ n'est pas applicable au C ; cependant, je ne parviens pas à comprendre comment il corrige le problème du C++ en sautant l'initialisation en premier lieu ? Ne saute-t-il pas encore la déclaration et l'affectation de newVal quand il saute à ANOTHER_VAL ?

18 votes

@legends2k : Oui, il le saute toujours. Cependant, quand je dis "il corrige le problème", je veux dire qu'il corrige l'erreur du compilateur C++ . En C++, il est illégal de sauter une déclaration scalaire. avec initialisateur mais il est parfaitement possible de ne pas déclarer un scalaire. sans initialisateur . Sur case ANOTHER_VAL: variable ponctuelle newVal est visible, mais avec une valeur indéterminée.

3 votes

Fascinant. J'ai trouvé cette question après avoir lu §A9.3: Compound Statement de K&R C (deuxième édition). L'entrée mentionne la définition technique d'un énoncé composé qui est {declaration-list[opt] statement-list[opt]} . Confus, parce que j'avais pensé qu'une déclaration était une déclaration, j'ai fait des recherches et j'ai immédiatement trouvé cette question, un exemple où cette disparité devient apparente et réelle. rupture un programme. Je pense qu'une autre solution (pour le C) serait de mettre une autre instruction (éventuellement une instruction null ?) avant la déclaration afin que le énoncé étiqueté est satisfaite.

142voto

Richard Corden Points 12292

Ok. Juste pour clarifier, cela n'a strictement rien à voir avec la déclaration. Il s'agit uniquement de "sauter par-dessus l'initialisation" (ISO C++ '03 6.7/3).

De nombreux messages ici ont mentionné que le fait de sauter par-dessus la déclaration peut avoir pour conséquence que la variable "n'est pas déclarée". Ceci n'est pas vrai. Un objet POD peut être déclaré sans initialisateur mais il aura une valeur indéterminée. Par exemple :

switch (i)
{
   case 0:
     int j; // 'j' has indeterminate value
     j = 0; // 'j' set (not initialized) to 0, but this statement
            // is jumped when 'i == 1'
     break;
   case 1:
     ++j;   // 'j' is in scope here - but it has an indeterminate value
     break;
}

Lorsque l'objet est un non-POD ou un agrégat, le compilateur ajoute implicitement un initialisateur, et il n'est donc pas possible de sauter par-dessus une telle déclaration :

class A {
public:
  A ();
};

switch (i)  // Error - jumping over initialization of 'A'
{
   case 0:
     A j;   // Compiler implicitly calls default constructor
     break;
   case 1:
     break;
}

Cette limitation ne se limite pas à l'énoncé du commutateur. L'utilisation de 'goto' pour sauter par-dessus une initialisation est également une erreur :

goto LABEL;    // Error jumping over initialization
int j = 0; 
LABEL:
  ;

Il s'agit d'une différence entre le C++ et le C. En C, ce n'est pas une erreur de sauter l'initialisation.

Comme d'autres l'ont mentionné, la solution consiste à ajouter un bloc imbriqué afin que la durée de vie de la variable soit limitée à l'étiquette du cas individuel.

2 votes

"Error jumping over initialization" ??? Pas avec mon GCC. Il peut donner un avertissement "j may be used unitialized" quand on utilise j sous label, mais il n'y a pas d'erreur. Cependant, dans le cas d'un switch, il y a une erreur (une erreur grave, pas un avertissement faible).

9 votes

@Mecki : C'est illégal en C++. ISO C++ '03 - 6.7/3 : "...Un programme qui saute d'un point où une variable locale 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 le type POD (3.9) et soit déclarée sans initialisateur (8.5)."

1 votes

Oui, mais ce n'est pas illégal en C (au moins gcc dit que ça ne l'est pas). j sera non initialisé (aura un certain nombre aléatoire), mais le compilateur le compile. Cependant, dans le cas de l'instruction switch, le compilateur ne la compilera même pas et je ne vois pas la différence entre un cas goto/label et un cas switch.

42voto

Mark Ingram Points 24995

L'ensemble de la déclaration de commutation est dans la même portée. Pour le contourner, faites ceci :

switch (val)
{
    case VAL:
    {
        // This **will** work
        int newVal = 42;
    }
    break;

    case ANOTHER_VAL:
      ...
    break;
}

Note les crochets.

21voto

emk Points 27772

Vous ne pouvez pas faire ça, parce que case Les étiquettes ne sont en fait que des points d'entrée dans le bloc contenant.

L'illustration la plus claire en est donnée par Dispositif de Duff . Voici un code tiré de Wikipedia :

strcpy(char *to, char *from, size_t count) {
    int 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);
    }
}

Remarquez comment le case Les étiquettes ignorent totalement les limites du bloc. Oui, c'est diabolique. Mais c'est pourquoi votre exemple de code ne fonctionne pas. Sauter à un case est identique à l'utilisation de l'étiquette goto Vous n'êtes donc pas autorisé à sauter par-dessus une variable locale avec un constructeur.

Comme l'ont indiqué plusieurs autres posters, vous devez mettre en place votre propre bloc :

switch (...) {
    case FOO: {
        MyObject x(...);
        ...
        break; 
    }
    ...
 }

1 votes

L'implémentation du dispositif de ce Duff a un bogue qui le rend extrêmement lent : count est de type int, donc le % doit effectuer une véritable opération de division/modulo. Rendez count non signé (ou mieux encore, utilisez toujours size_t pour les comptes/indices) et le problème disparaît.

1 votes

@R : Quoi ?! Dans un système de complément à deux, la signature n'affecte pas les modulos par puissances de 2 (c'est juste un AND sur les bits du bas), et n'affecte pas les divisions par puissances de 2 tant que l'architecture de votre processeur possède une opération arithmétique de décalage à droite ( SAR en x86, contre SHR qui est pour les décalages non signés).

0 votes

@Chris : Je pense qu'il veut dire quand le compilateur doit permettre des valeurs négatives où "juste un AND sur les bits du bas" ne tient pas ; par exemple, -1 % 8 donne -1 sur ce système de complément à deux utilisant g++ (le signe dans ce cas est une implémentation définie par 5.6/4).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X