Instrukcja Switch: czy wartość domyślna musi być ostatnim przypadkiem?

178

Rozważ następujące switchstwierdzenie:

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

Ten kod jest kompilowany, ale czy jest prawidłowy (= zdefiniowane zachowanie) dla C90 / C99? Nigdy nie widziałem kodu, w którym domyślny przypadek nie jest ostatnim przypadkiem.

EDYCJA:
Jak piszą Jon Cage i KillianDS : to jest naprawdę brzydki i zagmatwany kod i jestem tego świadomy. Interesuje mnie tylko ogólna składnia (czy jest zdefiniowana?) I oczekiwane dane wyjściowe.

tanascius
źródło
19
+1 Nigdy nawet nie brałem pod uwagę takiego zachowania
Jamie Wong
@ Péter Török: masz na myśli, że jeśli wartość == 2 zwróci 6?
Alexandre C.
4
@ Péter Török nie, kolejność nie ma znaczenia - jeśli wartość pasuje do stałej w którejkolwiek etykiecie przypadku, sterowanie przeskoczy do tej instrukcji po etykiecie, w przeciwnym razie control przeskoczy do instrukcji następującej po etykiecie domyślnej, jeśli jest obecna.
Pete Kirkham
11
@Jon Cage gotonie jest zły. Wyznawcy kultu Cargo są! Nie możesz sobie wyobrazić, do jakich skrajności ludzie mogą się posunąć, gotoponieważ jest to z całą pewnością tak złe, co powoduje prawdziwy nieczytelny bałagan w ich kodzie.
Patrick Schlüter
3
Używam gotogłównie do symulacji czegoś w rodzaju finallyklauzuli w funkcjach, gdzie zasoby (pliki, pamięć) muszą być zwolnione podczas zatrzymywania, a powtarzanie dla każdego przypadku błędu listy freei closenie pomaga w czytelności. Jest jednak jedno użyciegoto , którego chciałbym uniknąć, ale nie mogę, polega na tym, że chcę wyrwać się z pętli i jestem switchw tej pętli.
Patrick Schlüter

Odpowiedzi:

83

Standard C99 nie jest na ten temat jednoznaczny, ale biorąc wszystkie fakty razem, jest całkowicie ważny.

A casei defaultetykieta są odpowiednikiem gotoetykiety. Patrz 6.8.1 Oznakowane oświadczenia. Szczególnie interesujący jest 6.8.1.4, który umożliwia wspomniane już urządzenie Duffa:

Każda instrukcja może być poprzedzona przedrostkiem, który deklaruje identyfikator jako nazwę etykiety. Etykiety same w sobie nie zmieniają przepływu kontroli, która przebiega przez nie bez przeszkód.

Edycja : kod w przełączniku nie jest niczym specjalnym; jest to normalny blok kodu, jak w instrukcji if, z dodatkowymi etykietami skoku. To wyjaśnia zachowanie upadku i dlaczego breakjest to konieczne.

6.8.4.2.7 podaje nawet przykład:

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

W sztucznym fragmencie programu obiekt, którego identyfikatorem jest i, istnieje z automatycznym czasem trwania (w bloku), ale nigdy nie jest inicjowany, a zatem jeśli wyrażenie sterujące ma wartość różną od zera, wywołanie funkcji printf będzie miało dostęp do nieokreślonej wartości. Podobnie, wywołanie funkcji f nie może zostać osiągnięte.

Stałe wielkości liter muszą być unikalne w instrukcji switch:

6.8.4.2.3 Wyrażenie każdej etykiety przypadku będzie wyrażeniem stałym w postaci liczby całkowitej i żadne dwa wyrażenia stałe przypadku w tej samej instrukcji przełączającej nie będą miały takiej samej wartości po konwersji. Instrukcja switch może zawierać co najwyżej jedną etykietę domyślną.

Wszystkie przypadki są oceniane, a następnie przeskakuje do domyślnej etykiety, jeśli jest podana:

6.8.4.2.5 Awanse liczb całkowitych są przeprowadzane na wyrażeniu sterującym. Stałe wyrażenie w każdej etykiecie przypadku jest konwertowane na promowany typ wyrażenia sterującego. Jeśli przekonwertowana wartość jest zgodna z wartością promowanego wyrażenia sterującego, sterowanie przeskakuje do instrukcji następującej po dopasowanej etykiecie przypadku. W przeciwnym razie, jeśli istnieje etykieta domyślna, sterowanie przeskakuje do instrukcji z etykietą. Jeśli żadne przekształcone wyrażenie stałe wielkości liter nie pasuje i nie ma etykiety domyślnej, żadna część treści przełącznika nie jest wykonywana.

Bezpieczne
źródło
6
@HeathHunnicutt Najwyraźniej nie rozumiałeś celu przykładu. Kod nie jest wymyślony przez ten plakat, ale pochodzi bezpośrednio ze standardu C, jako ilustracja tego, jak dziwne są instrukcje przełącznika i jak zła praktyka prowadzi do błędów. Gdybyś zechciał przeczytać tekst pod kodem, zdałbyś sobie sprawę z tego.
Lundin
2
+1, aby zrekompensować głos przeciw. Negowanie kogoś z powoływania się na standard C wydaje się dość surowe.
Lundin
2
@Lundin Nie głosuję w dół na standard C i nie przeoczyłem niczego, jak sugerujesz. Głosowałem w dół za złą pedagogikę używania złego i niepotrzebnego przykładu. W szczególności przykład ten odnosi się do zupełnie innej sytuacji, o którą pytano. Mógłbym kontynuować, ale „dziękuję za opinię”.
Heath Hunnicutt
12
Firma Intel zaleca umieszczenie najczęściej używanego kodu jako pierwszego w instrukcji przełączania w ramach reorganizacji gałęzi i pętli, aby zapobiec nieszczęściom . Jestem tutaj, ponieważ mam defaultprzypadek dominujący nad innymi przypadkami o około 100: 1 i nie wiem, czy jest ważny, czy nieokreślony, aby zrobić defaultpierwszy przypadek.
jww
@jww Nie jestem pewien, co masz na myśli mówiąc o firmie Intel. Jeśli masz na myśli inteligencję, nazwę to hipotezą. Miałem to samo myślenie, ale późniejsze czytanie stwierdza, że ​​w przeciwieństwie do instrukcji if, instrukcje przełączające są dostępem swobodnym. Tak więc ostatni przypadek nie jest wolniejszy niż pierwszy. Osiąga się to poprzez haszowanie stałych wartości wielkości liter. Dlatego instrukcje przełączające są szybsze niż instrukcje if, gdy jest ich dużo.
91

Instrukcje case i instrukcja default mogą występować w dowolnej kolejności w instrukcji switch. Klauzula domyślna jest klauzulą ​​opcjonalną, która jest dopasowywana, jeśli żadna ze stałych w instrukcjach case nie może zostać dopasowana.

Dobry przykład :-

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


Outputs '2,default'

bardzo przydatne, jeśli chcesz, aby twoje sprawy były prezentowane w logicznej kolejności w kodzie (jak w nie mówiąc przypadek 1, przypadek 3, przypadek 2 / domyślny), a twoje sprawy są bardzo długie, więc nie chcesz powtarzać całej sprawy kod na dole dla domyślnego

Salil
źródło
7
To jest dokładnie ten scenariusz, w którym zwykle umieszczam wartość domyślną w innym miejscu niż koniec ... istnieje logiczna kolejność jawnych przypadków (1, 2, 3) i chcę, aby domyślny zachowywał się dokładnie tak samo, jak jeden z jawnych przypadków, które nie ostatni.
ArtOfWarfare
51

W niektórych przypadkach jest to ważne i bardzo przydatne.

Rozważ następujący kod:

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
}

Chodzi o to, że powyższy kod jest bardziej czytelny i wydajny niż kaskadowy if. Mógłbyś to defaultzakończyć, ale jest to bezcelowe, ponieważ skupi twoją uwagę na przypadkach błędów zamiast normalnych przypadków (tak jest w tym defaultprzypadku).

Właściwie to nie jest taki dobry przykład, ponieważ pollwiesz, ile zdarzeń może wystąpić najwyżej. Moje prawdziwe jest to, że tam przypadki z określonego zestawu wartości wejściowych, gdzie istnieją „wyjątki” i normalnych przypadkach. Czy lepiej jest umieścić wyjątki lub normalne przypadki na pierwszym planie, to kwestia wyboru.

W dziedzinie oprogramowania myślę o innym bardzo zwykłym przypadku: rekurencji z pewnymi wartościami końcowymi. Jeśli możesz to wyrazić za pomocą przełącznika,default będzie zwykłą wartością zawierającą wywołanie rekurencyjne i wyróżnione elementy (pojedyncze przypadki) wartości końcowe. Zwykle nie ma potrzeby skupiania się na wartościach końcowych.

Innym powodem jest to, że kolejność przypadków może zmienić zachowanie skompilowanego kodu, co ma znaczenie dla wydajności. Większość kompilatorów wygeneruje skompilowany kod asemblera w tej samej kolejności, w jakiej kod pojawia się w przełączniku. To sprawia, że ​​pierwszy przypadek bardzo różni się od pozostałych: wszystkie przypadki z wyjątkiem pierwszego będą wymagały przeskoku, co spowoduje opróżnienie potoków procesora. Możesz to rozumieć tak, jak predyktor gałęzi, który domyślnie uruchamia pierwszy występujący przypadek w przełączniku. Jeśli przypadek jest znacznie bardziej powszechny niż inne, masz bardzo dobre powody, aby umieścić go jako pierwszy.

Czytanie komentarzy jest to szczególny powód, dla którego oryginalny plakat zadał to pytanie po przeczytaniu reorganizacji Branch Loop kompilatora Intel na temat optymalizacji kodu.

Wtedy stanie się to arbitrażem między czytelnością i wydajnością kodu. Prawdopodobnie lepiej jest zamieścić komentarz wyjaśniający przyszłemu czytelnikowi, dlaczego przypadek pojawia się jako pierwszy.

kriss
źródło
6
+1 za podanie (dobrego) przykładu bez zachowania upadku.
KillianDS
1
... myśląc o tym, nie jestem przekonany, że ustawienie domyślne na górze jest dobre, ponieważ bardzo niewielu ludzi by go tam szukało. Lepiej byłoby przypisać powrót do zmiennej i obsłużyć sukces po jednej stronie if i błędy po drugiej stronie za pomocą instrukcji case.
Jon Cage
@Jon: po prostu to napisz. Dodajesz szum składniowy bez żadnej korzyści z czytelności. A jeśli wartość domyślna znajduje się na górze, naprawdę nie ma potrzeby na to patrzeć, jest to naprawdę oczywiste (może być trudniejsze, jeśli umieścisz to pośrodku).
kriss
Nawiasem mówiąc, nie podoba mi się składnia przełącznika / wielkości liter w C. Wolałbym mieć możliwość umieszczania kilku etykiet po jednym przypadku, zamiast być zmuszonym do umieszczania kilku kolejnych case. Przygnębiające jest to, że wygląda tak samo jak cukier składniowy i nie zepsuje żadnego istniejącego kodu, jeśli jest obsługiwany.
kriss
1
@kriss: Kusiło mnie, żeby powiedzieć „Ja też nie jestem programistą Pythona!” :)
Andrew Grimm
16

tak, to jest ważne, aw pewnych okolicznościach nawet przydatne. Generalnie, jeśli tego nie potrzebujesz, nie rób tego.

Jens Gustedt
źródło
-1: To pachnie dla mnie złem. Byłoby lepiej podzielić kod na parę instrukcji switch.
Jon Cage,
25
@John Cage: postawienie mi -1 tutaj jest paskudne. To nie moja wina, że ​​jest to prawidłowy kod.
Jens Gustedt
po prostu ciekawy, chciałbym wiedzieć, w jakich okolicznościach jest przydatny?
Salil
1
-1 miał na celu zapewnienie, że jest przydatny. Zmienię go na +1, jeśli możesz podać prawidłowy przykład na poparcie swojego roszczenia.
Jon Cage,
4
Czasami podczas przełączania na errno, które otrzymaliśmy w zamian za jakąś funkcję systemową. Powiedzmy, że mamy jeden przypadek, w którym wiemy na dobre, że musimy wykonać czyste wyjście, ale to czyste wyjście może wymagać pewnych linii kodu, których nie chcemy powtarzać. Ale przypuśćmy, że mamy również wiele innych egzotycznych kodów błędów, z którymi nie chcemy obsługiwać indywidualnie. Rozważałbym po prostu umieszczenie błędu w domyślnym przypadku i pozwolenie mu przejść do drugiego przypadku i wyjść bezproblemowo. Nie mówię, że powinieneś to robić w ten sposób. To tylko kwestia gustu.
Jens Gustedt
8

W instrukcji switch nie ma zdefiniowanej kolejności. Możesz patrzeć na przypadki jako na coś w rodzaju nazwanej etykiety, jak gotoetykieta. W przeciwieństwie do tego, co ludzie wydają się tutaj sądzić, w przypadku wartości 2 nie przeskakuje się do domyślnej etykiety. Aby zilustrować klasycznym przykładem, oto urządzenie Duffa , które jest plakatem potomnym skrajności switch/casew 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);
  }
}
Patrick Schlüter
źródło
4
A dla każdego, kto nie jest zaznajomiony z urządzeniem Duffa, ten kod jest całkowicie nieczytelny ...
KillianDS
7

Jeden scenariusz, w którym uznałbym za stosowne umieszczenie „wartości domyślnej” w innym miejscu niż instrukcja końca przypadku, dotyczy automatu stanowego, w którym nieprawidłowy stan powinien zresetować maszynę i postępować tak, jakby był to stan początkowy. Na przykład:

przełącznik (stan_widżetu)
{
  domyślnie: / * Spadł z szyn - zresetuj i kontynuuj * /
    widget_state = WIDGET_START;
    / * Spadaj * /
  sprawa WIDGET_START:
    ...
    przerwa;
  etui WIDGET_WHATEVER:
    ...
    przerwa;
}

rozwiązanie alternatywne, jeśli nieprawidłowy stan nie powinien resetować maszyny, ale powinien być łatwo rozpoznawalny jako nieprawidłowy stan:

przełącznik (stan_widżetu) { sprawa WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); przerwa; sprawa WIDGET_START: ... przerwa; etui WIDGET_WHATEVER: ... przerwa; domyślna: widget_state = WIDGET_INVALID_STATE; / * Spadaj * / sprawa WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... zrób wszystko, co konieczne, aby ustanowić „bezpieczny” stan }

Kod w innym miejscu może następnie sprawdzić (widget_state == WIDGET_INVALID_STATE) i zapewnić wszelkie działania związane z raportowaniem błędów lub resetowaniem stanu, które wydają się odpowiednie. Na przykład kod paskowy stanu może wyświetlać ikonę błędu, a opcja menu „start widget”, która jest wyłączona w większości stanów bezczynności, może być włączona zarówno dla WIDGET_INVALID_STATE, jak i WIDGET_IDLE.

superkat
źródło
6

Wbijając inny przykład: może to być przydatne, jeśli „domyślny” jest nieoczekiwanym przypadkiem i chcesz zarejestrować błąd, ale także zrobić coś rozsądnego. Przykład z mojego własnego kodu:

  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
źródło
5

Istnieją przypadki, gdy konwertujesz ENUM na ciąg lub konwertujesz ciąg na wyliczenie w przypadku, gdy piszesz / czytasz do / z pliku.

Czasami trzeba ustawić jedną z wartości jako domyślną, aby pokryć błędy popełnione podczas ręcznej edycji plików.

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

case ModeStretched:
    // write to a file "stretched"
    break;
}
Predrag Manojlovic
źródło
2

defaultWarunek może być wszędzie w przełączniku, że klauzula przypadek może istnieć. Nie jest wymagane, aby była to ostatnia klauzula. Widziałem kod, który ustawił wartość domyślną jako pierwszą klauzulę. Jest case 2:wykonywany normalnie, mimo że klauzula default jest nad nim.

W ramach testu umieściłem przykładowy kod w funkcji, wywołałem test(int value){}i uruchomiłem:

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

Wynik to:

0=2
1=1
2=4
3=8
4=10
Scott Thomson
źródło
1

Jest ważny, ale raczej paskudny. Sugerowałbym, że generalnie złe jest zezwalanie na upadki, ponieważ może to prowadzić do bardzo niechlujnego kodu spaghetti.

Prawie na pewno lepiej jest rozbić te przypadki na kilka instrukcji switch lub mniejszych funkcji.

[edytuj] @Tristopia: Twój przykład:

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

byłoby jaśniejsze co do jego intencji (myślę), gdyby zostało napisane w ten sposób:

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: Twój drugi przykład jest prawdopodobnie najczystszym przykładem dobrego wykorzystania do kontynuacji:

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

... ale osobiście podzieliłbym rozpoznawanie komentarzy na jego własną funkcję:

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
źródło
2
Są przypadki, w których upadek jest naprawdę dobrym pomysłem.
Patrick Schlüter
Przykład konwersji UCS-2 na UTF-8 rto tablica docelowa, wcto wchar_t przełącznik wejściowy (utf8_length) {/ * Uwaga: kod przechodzi przez przypadki! * / przypadek 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; przypadek 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; przypadek 1: r [0] = wc; }
Patrick Schlüter
Tutaj kolejna procedura kopiowania ciągu znaków ze znakami ucieczki: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter
Tak, ale ta procedura jest jednym z naszych hotspotów, był to najszybszy, przenośny (nie będziemy montować) sposób na jej wdrożenie. Ma tylko 1 test na dowolną długość UTF, twój ma 2 lub nawet 3. Poza tym nie wymyśliłem tego, wziąłem go z BSD.
Patrick Schlüter
1
Tak, były, zwłaszcza w konwersjach w języku bułgarskim i greckim (na Solarisie SPARC) oraz w tekście z naszym wewnętrznym znacznikiem (czyli 3 bajtowym UTF8). Przyznał, że w sumie to niewiele i stało się nieistotne od czasu naszej ostatniej aktualizacji sprzętu, ale w czasie, gdy to pisano, miało to pewne znaczenie.
Patrick Schlüter