web-dev-qa-db-fra.com

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

Je me suis toujours demandé pourquoi. Pourquoi ne pouvez-vous pas déclarer des variables après une étiquette de cas dans une instruction switch? En C++, vous pouvez déclarer des variables à peu près n'importe où (et les déclarer proches de la première utilisation est évidemment une bonne chose), mais les suivantes ne fonctionnent 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 le libellé 'case'

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

890
Rob

Les instructions Case sont uniquement étiquettes. Cela signifie que le compilateur interprétera cela comme un saut directement vers l'étiquette. En C++, le problème 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'instruction switch. Cela signifie que vous vous retrouvez avec une étendue dans laquelle un saut sera effectué plus loin dans le code en ignorant l'initialisation. La bonne façon de gérer cela consiste à définir une étendue spécifique à cette instruction case et à y définir votre variable.

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

Cette question est était à l'origine étiqueté comme [C] et [C++] en même temps. Le code original est en effet invalide en C et en C++, mais pour des raisons totalement différentes et sans rapport.

  • En C++, ce code n'est pas valide car l'étiquette case ANOTHER_VAL: passe dans l'étendue de la variable newVal en ignorant son initialisation. Les sauts qui ignorent l'initialisation des objets automatiques sont interdits en C++. Cet aspect de la question est correctement traité par la plupart des réponses.

  • Cependant, en langage C, l'initialisation des variables n'est pas une erreur. Entrer dans le champ d'application d'une variable lors de son initialisation est légal en C. Cela signifie simplement que la variable est laissée non initialisée. Le code d'origine ne compile pas en C pour une raison complètement différente. L'étiquette case VAL: dans le code d'origine est attachée à la déclaration de la variable newVal. En langage C, les déclarations ne sont pas des déclarations. Ils ne peuvent pas être étiquetés. Et c'est ce qui cause l'erreur lorsque ce code est interprété en tant que code C.

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

L'ajout d'un bloc {} supplémentaire résout les problèmes C++ et C, même si ces problèmes sont très différents. Du côté C++, cela restreint la portée de newVal, en s'assurant que case ANOTHER_VAL: ne saute plus dans cette portée, ce qui élimine le problème C++. Du côté C, cet excédent {} introduit une instruction composée, ce qui permet d'appliquer l'étiquette case VAL: à une instruction, ce qui élimine le problème de C.

  • En C, le problème peut être facilement résolu sans le {}. Ajoutez simplement une instruction vide après l’étiquette 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 s'il est maintenant valide du point de vue C, il reste invalide du point de vue C++.

  • Symétriquement, en C++, le problème peut être facilement résolu sans le {}. Supprimez simplement l'initialiseur de la déclaration de 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 s'il est maintenant valide du point de vue C++, il reste invalide du point de vue C.

307
AnT

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

Beaucoup de messages ici ont mentionné que sauter par dessus la déclaration peut avoir pour conséquence que la variable "ne soit pas déclarée". Ce n'est pas vrai. Un objet POD peut être déclaré sans initialiseur, mais sa valeur sera indéterminée. Par exemple:

switch (i)
{
   case 0:
     int j; // 'j' has indeterminate value
     j = 0; // 'j' 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 initialiseur. Il est donc impossible 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 n'est pas limitée à l'instruction switch. Utiliser 'goto' pour sauter une initialisation est également une erreur:

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

Un détail est que c'est une différence entre C++ et C. En C, ce n'est pas une erreur de sauter par-dessus 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 de cas individuel.

131
Richard Corden

Toute la déclaration de commutateur est dans la même portée. Pour le contourner, procédez comme suit:

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

    case ANOTHER_VAL:
      ...
    break;
}

Note les crochets.

35
Mark Ingram

Après avoir lu toutes les réponses et fait d’autres recherches, j’obtiens quelques petites choses.

Case statements are only 'labels'

En C, conformément à la spécification,

§6.8.1 Déclarations étiquetées:

labeled-statement:
    identifier : statement
    case constant-expression : statement
    default : statement

En C, aucune clause ne permet une "déclaration étiquetée". Cela ne fait tout simplement pas partie de la langue.

Alors

case 1: int x=10;
        printf(" x is %d",x);
break;

Ceci ne sera pas compilé , voir http://codepad.org/YiyLQTYw . GCC donne une erreur:

label can only be a part of statement and declaration is not a statement

Même

  case 1: int x;
          x=10;
            printf(" x is %d",x);
    break;

ceci est également non compilé , voir http://codepad.org/BXnRD3b . Ici, je reçois aussi la même erreur.


En C++, conformément à la spécification,

labellisé-déclaration est autorisé mais étiqueté -initialization n'est pas autorisé.

Voir http://codepad.org/ZmQ0IyDG .


La solution à cette condition est deux

  1. Soit utiliser une nouvelle portée en utilisant {}

    case 1:
           {
               int x=10;
               printf(" x is %d", x);
           }
    break;
    
  2. Ou utilisez une instruction factice avec une étiquette

    case 1: ;
               int x=10;
               printf(" x is %d",x);
    break;
    
  3. Déclarez la variable avant switch () et initialisez-la avec des valeurs différentes dans l'instruction case si elle répond à vos besoins.

    main()
    {
        int x;   // Declare before
        switch(a)
        {
        case 1: x=10;
            break;
    
        case 2: x=20;
            break;
        }
    }
    

Quelques éléments supplémentaires avec l'instruction switch

N'écrivez jamais dans le commutateur des instructions qui ne font partie d'aucune étiquette, car elles ne seront jamais exécutées:

switch(a)
{
    printf("This will never print"); // This will never executed

    case 1:
        printf(" 1");
        break;

    default:
        break;
}

Voir http://codepad.org/PA1quYX .

28
Jeegar Patel

Vous ne pouvez pas faire cela car les étiquettes case ne sont en réalité que des points d'entrée dans le bloc conteneur.

Ceci est le plus clairement illustré par le dispositif de Duff . Voici quelques codes 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);
    }
}

Notez que les étiquettes case ignorent totalement les limites du bloc. Oui c'est mal. Mais c’est pourquoi votre exemple de code ne fonctionne pas. Passer à une étiquette case revient à utiliser goto, vous n'êtes donc pas autorisé à sauter par-dessus une variable locale avec un constructeur.

Comme plusieurs autres affiches l'ont indiqué, vous devez créer un bloc:

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

Jusqu'à présent, la plupart des réponses sont fausses sur un point: vous pouvez déclarez des variables après l'instruction case, mais vous ne pouvez pas les initialiser:

case 1:
    int x; // Works
    int y = 0; // Error, initialization is skipped by case
    break;
case 2:
    ...

Comme mentionné précédemment, une bonne solution consiste à utiliser des accolades pour créer une portée pour votre cas.

16
MrZebra

Mon astuce diabolique préférée consiste à utiliser un if (0) pour ignorer une étiquette de cas non désirée.

switch(val)
{
case 0:
// Do something
if (0) {
case 1:
// Do something else
}
case 2:
// Do something in all cases
}

Mais très méchant.

12
Jeremy

Essaye ça:

switch (val)
{
    case VAL:
    {
        int newVal = 42;
    }
    break;
}
10
Dan Shield

Vous pouvez déclarer des variables dans une instruction switch if vous commencez un nouveau bloc:

switch (thing)
{ 
  case A:
  {
    int i = 0;  // Completely legal
  }
  break;
}

La raison en est d’allouer (et de récupérer) de l’espace sur la pile pour le stockage de la ou des variables locales.

7
Seb Rose

Considérer:

switch(val)
{
case VAL:
   int newVal = 42;
default:
   int newVal = 23;
}

En l'absence d'énoncés break, newVal est parfois déclarée deux fois et vous ne savez pas si c'est le cas jusqu'au moment de l'exécution. Je suppose que la limitation est due à ce type de confusion. Quelle serait la portée de newVal? La convention voudrait que ce soit l’ensemble du bloc de commutation (entre les accolades).

Je ne suis pas un programmeur C++, mais en C:

switch(val) {
    int x;
    case VAL:
        x=1;
}

Fonctionne bien. Déclarer une variable à l'intérieur d'un bloc de commutateur est acceptable. Déclarer après une garde de cas n'est pas.

6
slim

La section entière du commutateur est un contexte de déclaration unique. Vous ne pouvez pas déclarer une variable dans une instruction case comme ça. Essayez ceci à la place:

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

Si votre code indique "int newVal = 42", vous vous attendez donc à ce que newVal ne soit jamais non initialisé. Mais si vous passez au-dessus de cette déclaration (ce que vous êtes en train de faire), c'est exactement ce qui se produit - newVal est dans la portée, mais n'a pas été attribué.

Si c'est ce que vous vouliez vraiment faire, le langage exige de l'expliciter en disant "int newVal; newVal = 42;". Sinon, vous pouvez limiter la portée de newVal à un seul cas, ce qui correspond probablement à ce que vous souhaitiez.

Cela peut clarifier les choses si vous considérez le même exemple mais avec "const int newVal = 42;"

3
Mike F

Je voulais juste souligner slim 's point . Une construction de commutateur crée une portée entière, citoyen de première classe. Il est donc possible de déclarer (et d’initialiser) une variable dans une instruction switch avant la première étiquette de cas, sans une paire de crochets supplémentaire:

switch (val) {  
  /* This *will* work, even in C89 */
  int newVal = 42;  
case VAL:
  newVal = 1984; 
  break;
case ANOTHER_VAL:  
  newVal = 2001;
  break;
}
3
VictorH

Intéressant que cela va bien:

switch (i)  
{  
case 0:  
    int j;  
    j = 7;  
    break;  

case 1:  
    break;
}

... mais ce n'est pas:

switch (i)  
{  
case 0:  
    int j = 7;  
    break;  

case 1:  
    break;
}

Je comprends qu'un correctif est assez simple, mais je ne comprends pas encore pourquoi le premier exemple ne gêne pas le compilateur. Comme il a été mentionné précédemment (2 ans plus tôt), déclaration n’est pas ce qui cause l’erreur, même malgré la logique. L'initialisation est le problème. Si la variable est initialisée et déclarée sur les différentes lignes, elle est compilée.

3
Dan

J'ai écrit cette réponse orginalement pour cette question . Cependant, quand j'ai fini, j'ai trouvé que la réponse avait été fermée. Alors je l'ai posté ici, peut-être que quelqu'un qui aime les références aux standards le trouvera utile.

Code original en question:

int i;
i = 2;
switch(i)
{
    case 1: 
        int k;
        break;
    case 2:
        k = 1;
        cout<<k<<endl;
        break;
}

Il y a en fait 2 questions:

1. Pourquoi puis-je déclarer une variable après le libellé case?

C'est parce que dans C++, l'étiquette doit être sous forme:

N3337 6.1/1

déclaration étiquetée:

...

  • attribut-spécificateur-seqopt ​​caseconstant-expression: statement

...

Et dans C++instruction de déclaration est également considéré comme instruction (par opposition à C):

N3337 6/1:

déclaration:

...

déclaration-déclaration

...

2. Pourquoi est-ce que je peux sauter par-dessus une déclaration de variable puis l’utiliser?

Parce que: N3337 6.7/3

Il est possible de transférer dans un bloc , mais pas de manière à contourner les déclarations avec initialisation . Un programme qui saute (Le transfère de la condition d’une instruction switch à une étiquette de cas est considéré comme un saut à cet égard.)

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

Puisque k est de type scalaire, et n'est pas initialisé au moment de la déclaration, il est possible de sauter par dessus sa déclaration. Ceci est sémantiquement équivalent:

goto label;

int x;

label:
cout << x << endl;

Cependant, cela ne serait pas possible si x était initialisé au moment de la déclaration:

 goto label;

    int x = 58; //error, jumping over declaration with initialization

    label:
    cout << x << endl;
3
PcAF

Jusqu'à présent, les réponses ont été pour C++.

Pour C++, vous ne pouvez pas sauter par-dessus une initialisation. Vous pouvez le faire en C. Cependant, en C, une déclaration n'est pas une déclaration et les libellés de casse doivent être suivis par des déclarations.

Donc, valide (mais moche) C, invalide C++

switch (something)
{
  case 1:; // Ugly hack empty statement
    int i = 6;
    do_stuff_with_i(i);
    break;
  case 2:
    do_something();
    break;
  default:
    get_a_life();
}

Inversement, en C++, une déclaration est une instruction. Par conséquent, ce qui suit est valide C++, C non valide.

switch (something)
{
  case 1:
    do_something();
    break;
  case 2:
    int i = 12;
    do_something_else();
}
3
Peter

Les nouvelles variables peuvent être déclarées uniquement à la portée du bloc. Vous devez écrire quelque chose comme ceci:

case VAL:  
  // This will work
  {
  int newVal = 42;  
  }
  break;

Bien sûr, newVal n’a de portée que dans les accolades ...

À la vôtre, Ralph

1
Ralph Hempel

Un bloc switchce n'est pas la même chose qu'une succession de blocs if/else if. Je suis surpris qu'aucune autre réponse ne l'explique clairement.

Considérez cette déclaration switch:

switch (value) {
    case 1:
        int a = 10;
        break;
    case 2:
        int a = 20;
        break;
}

Cela peut paraître surprenant, mais le compilateur ne le verra pas comme un simple if/else if. Il produira le code suivant:

if (value == 1)
    goto label_1;
else if (value == 2)
    goto label_2;
else
    goto label_end;

{
label_1:
    int a = 10;
    goto label_end;
label_2:
    int a = 20; // Already declared !
    goto label_end;
}

label_end:
    // The code after the switch block

Les instructions case sont converties en étiquettes, puis appelées avec goto. Les crochets créent une nouvelle portée et il est facile de voir maintenant pourquoi vous ne pouvez pas déclarer deux variables du même nom dans un bloc switch.

Cela peut paraître bizarre, mais il est nécessaire de prendre en charge fallthrough (c’est-à-dire de ne pas utiliser break pour laisser l'exécution se poursuivre jusqu'au prochain case).

1
Dalmas

newVal existe dans toute l'étendue du commutateur mais n'est initialisé que si le membre VAL est touché. Si vous créez un bloc autour du code dans VAL, cela devrait être OK.

0
marijne

C++ Standard a: Il est possible de transférer dans un bloc, mais pas de manière à contourner les déclarations avec initialisation. 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é, sauf si la variable a le type POD (3.9) et est déclarée sans initialiseur (8.5).

Le code pour illustrer cette règle:

#include <iostream>

using namespace std;

class X {
  public:
    X() 
    {
     cout << "constructor" << endl;
    }
    ~X() 
    {
     cout << "destructor" << endl;
    }
};

template <class type>
void ill_formed()
{
  goto lx;
ly:
  type a;
lx:
  goto ly;
}

template <class type>
void ok()
{
ly:
  type a;
lx:
  goto ly;
}

void test_class()
{
  ok<X>();
  // compile error
  ill_formed<X>();
}

void test_scalar() 
{
  ok<int>();
  ill_formed<int>();
}

int main(int argc, const char *argv[]) 
{
  return 0;
}

Le code pour montrer l'effet d'initialisation:

#include <iostream>

using namespace std;

int test1()
{
  int i = 0;
  // There jumps fo "case 1" and "case 2"
  switch(i) {
    case 1:
      // Compile error because of the initializer
      int r = 1; 
      break;
    case 2:
      break;
  };
}

void test2()
{
  int i = 2;
  switch(i) {
    case 1:
      int r;
      r= 1; 
      break;
    case 2:
      cout << "r: " << r << endl;
      break;
  };
}

int main(int argc, const char *argv[]) 
{
  test1();
  test2();
  return 0;
}
0
Jingguo Yao