Używanie {} w instrukcji case. Czemu?

101

Jaki jest sens używania {i }w caseoświadczeniu? Zwykle, bez względu na to, ile wierszy jest w caseinstrukcji, wszystkie wiersze są wykonywane. Czy to tylko reguła dotycząca starszych / nowszych kompilatorów, czy też coś za tym stoi?

int a = 0;
switch (a) {
  case 0:{
    std::cout << "line1\n";
    std::cout << "line2\n";
    break;
  }
}

i

int a = 0;
switch (a) {
  case 0:
    std::cout << "line1\n";
    std::cout << "line2\n";
    break;
}
mahmood
źródło
57
Jednym z zastosowań może być ograniczenie zakresu zmiennych zadeklarowanych w instrukcji case.
Abhishek Bansal
1
Za dużo wcięć. Przypadki są po prostu etykietami w bloku instrukcji switch: nie wprowadzają dodatkowego zagnieżdżenia, więc powinny być zgodne ze switchsłowem kluczowym, aw drugim przykładzie załączone instrukcje są wcięte tylko raz. Zwróć uwagę na niewygodne wcięcie o cztery spacje po break;.
Kaz
Zauważ, że zaakceptowana odpowiedź jest tylko częściowo poprawna, ponieważ komentarz Jacka wskazuje i pomija pewne subtelności, do których odwołuję się w mojej odpowiedzi.
Shafik Yaghmour
Tak jak FYI: w C (nawet C11) zamiast C ++ nie można oznaczyć deklaracji etykietą; nie należą do kategorii syntaktycznej statement. W C ++ możesz (jednym ze składników kategorii syntaktycznej statementjest declaration statement).
Jonathan Leffler,

Odpowiedzi:

195

{}Oznacza nowy blok zakresu .

Rozważmy następujący bardzo wymyślny przykład:

switch (a)
{
    case 42:
        int x = GetSomeValue();
        return a * x;
    case 1337:
        int x = GetSomeOtherValue(); //ERROR
        return a * x;
}

Pojawi się błąd kompilatora, ponieważ xjest już zdefiniowany w zakresie.

Oddzielenie ich do ich własnego podzakresu eliminuje potrzebę deklarowania xpoza instrukcją switch.

switch (a)
{
    case 42: {
        int x = GetSomeValue();
        return a * x; 
    }
    case 1337: {
        int x = GetSomeOtherValue(); //OK
        return a * x; 
    }
}
Rotem
źródło
11
Właściwie IMO otrzymasz błąd kompilatora, nawet jeśli pominiesz drugą deklarację zmiennej x.
Abhishek Bansal
1
Chociaż nadużywanie tego stylu i umieszczanie dużych bloków wewnątrz instrukcji switch sprawi, że śledzenie przypadków będzie nieczytelne. Wolę, aby wypowiedzi były niewielkie.
masoud
2
@MatthieuM. Wiem na pewno, że MS Visual Studio 2010 będzie zachowywał się tak, jak wskazuje Abhishek: nie będzie kompilował żadnej deklaracji zmiennej wewnątrz przypadku (chyba że użyjesz nawiasów klamrowych do oznaczenia nowego zakresu w tym przypadku). Czy to pasuje do standardów, nie wiem.
KRyan
1
@KRyan: nie, ale jest to o wiele bezpieczniejsza alternatywa, że ​​nie mogę ich winić za egzekwowanie tego.
Matthieu M.
6
Rozdział 6.7 (3) normy (numeracja dla wersji roboczej 2005) określa, że ​​nie można przeskoczyć inicjalizacji, więc nie można mieć inicjalizacji w bloku przypadku.
Jack Aidley
23

TL; DR

Jedynym sposobem, w jaki można zadeklarować zmienną z intializatorem lub jakimś nietrywialnym obiektem wewnątrz przypadku, jest wprowadzenie zakresu blokowego przy użyciu {}lub innej struktury kontrolnej, która ma własny zakres, jak pętla lub instrukcja if .

Krwawe szczegóły

Widzimy, że przypadki są po prostu etykietowanymi instrukcjami, takimi jak etykiety używane z instrukcją goto ( jest to omówione w szkicu standardu C ++ w sekcji 6.1 Oznakowane instrukcje ) i widzimy z sekcji 6.7paragraf 3, że przeskakiwanie do deklaracji jest w wielu przypadkach niedozwolone , w tym te z inicjalizacją:

Możliwe jest przeniesienie do bloku, ale nie w sposób omijający deklaracje przy inicjalizacji. Program, który przeskakuje 87 z punktu, w którym zmienna z automatycznym czasem przechowywania nie znajduje się w zakresie, do punktu, w którym jest w zakresie, jest źle sformułowana, chyba że zmienna ma typ skalarny, typ klasy z trywialnym konstruktorem domyślnym i trywialnym destruktorem, kwalifikowana wersja cv jednego z tych typów lub tablica jednego z powyższych typów i jest zadeklarowana bez inicjatora (8.5).

i podaje ten przykład:

void f() {
 // ...
 goto lx; // ill-formed: jump into scope of a

 ly:
  X a = 1;
 // ...
 lx:
  goto ly; // OK, jump implies destructor
          // call for a followed by construction
          // again immediately following label ly
}

Zauważ, że są tutaj pewne subtelności, możesz przeskoczyć poza deklarację skalarną , która nie ma inicjalizacji, na przykład:

switch( n ) 
{
    int x ;
    //int x  = 10 ; 
    case 0:
      x = 0 ;
      break;
    case 1:
      x = 1 ;
      break;
    default:
      x = 100 ;
      break ;
}

jest całkowicie poprawny ( przykład na żywo ). Oczywiście, jeśli chcesz zadeklarować tę samą zmienną w każdym przypadku, każda z nich będzie potrzebować własnego zakresu, ale działa to tak samo również poza instrukcjami switch , więc nie powinno to być dużym zaskoczeniem.

Jeśli chodzi o uzasadnienie niedopuszczania do przeskoczenia po inicjalizacji, raport defektu 467, chociaż obejmuje nieco inny problem, zapewnia rozsądny argument dla zmiennych automatycznych :

[...] zmienne automatyczne, jeśli nie są jawnie zainicjowane, mogą mieć nieokreślone („śmieciowe”) wartości, w tym reprezentacje pułapek, [...]

Prawdopodobnie bardziej interesujące jest przyjrzenie się przypadkowi, w którym rozszerza się zakres w ramach przełączania na wiele przypadków, najsłynniejszym tego przykładem jest prawdopodobnie urządzenie Duffa, które wyglądałoby mniej więcej tak:

void send( int *to, const int *from, int  count)
{
        int n = (count + 7) / 8;
        switch(count % 8) 
        {
            case 0: do {    *to = *from++;   // <- Scope start
            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);    // <- Scope end
        }
}
Shafik Yaghmour
źródło
6

Jest to nawyk, który pozwala na wstrzykiwanie deklaracji zmiennych z wynikowym destruktorem (lub konfliktami zakresu) do caseklauzul. Innym sposobem patrzenia na to jest to, że piszą w języku, który chcieliby mieć, w którym cała kontrola przepływu składa się z bloków, a nie sekwencji instrukcji.

Yakk - Adam Nevraumont
źródło
4

Sprawdź to podstawowe ograniczenie kompilatora, a zaczniesz się zastanawiać, co się dzieje:

int c;
c=1;

switch(c)
{
    case 1:
    //{
        int *i = new int;
        *i =1;
        cout<<*i;
        break;
    //}

    default : cout<<"def";
}

To spowoduje błąd:

error: jump to case label [-fpermissive]
error:   crosses initialization of int* i

Chociaż ten nie będzie:

int c;
c=1;

switch(c)
{
    case 1:
    {
        int *i = new int;
        *i =1;
        cout<<*i;
        break;
    }

    default : cout<<"def";
}
pj
źródło
1

Używanie nawiasów w przełączniku oznacza nowy blok zakresu, jak powiedział Rotem.

Ale może to być również dla jasności podczas czytania. Aby wiedzieć, gdzie kończy się sprawa, ponieważ możesz mieć w niej warunkową przerwę.

barwniki
źródło
0

Powody mogą być następujące:

  1. Czytelność, wizualnie poprawia każdą sprawę jako sekcję o określonym zakresie.
  2. Deklarowanie różnych zmiennych o tej samej nazwie dla kilku przypadków przełączania.
  3. Mikrooptymalizacje - zakres dla naprawdę kosztownej zmiennej alokowanej w zasobach, którą chcesz zniszczyć, gdy tylko opuścisz zakres sprawy, lub nawet bardziej spaghetti scenariusz użycia polecenia "GOTO".
Roman Ambinder
źródło