Czy użycie if (0) do pominięcia przypadku w przełączniku powinno działać?

122

Mam sytuację, w której chciałbym, aby dwa przypadki w instrukcji przełącznika C ++ przechodziły do ​​trzeciego przypadku. W szczególności druga sprawa przechodziłaby do trzeciej sprawy, a pierwsza sprawa również przechodziłaby do trzeciej sprawy bez przechodzenia przez drugą sprawę.

Miałem głupi pomysł, wypróbowałem go i zadziałało! I owinięty drugi przypadek w if (0) {... }. To wygląda tak:

#ifdef __cplusplus
#  include <cstdio>
#else
#  include <stdio.h>
#endif

int main(void) {
    for (int i = 0; i < 3; i++) {
        printf("%d: ", i);
        switch (i) {
        case 0:
            putchar('a');
            // @fallthrough@
            if (0) {        // fall past all of case 1 (!)
        case 1:
            putchar('b');
            // @fallthrough@
            }
        case 2:
            putchar('c');
            break;
        }
        putchar('\n');
    }
    return 0;
}

Po uruchomieniu otrzymuję żądany wynik:

0: ac
1: bc
2: c

Wypróbowałem to zarówno w C, jak i C ++ (oba z clang) i zrobiło to samo.

Moje pytania to: czy to jest poprawne C / C ++? Czy ma robić to, co robi?

Mark Adler
źródło
34
Tak, jest to ważne i działa z prawie tych samych powodów, dla których działa urządzenie Duff .
dxiv
42
Zwróć uwagę, że taki kod sprawi, że zostaniesz wyrzucony z dowolnego przewodnika po kodzie w środowisku, które dba o czytelność i łatwość utrzymania.
Andrew Henle
16
To jest okropne. Pamiętaj, nawet straszniejsze niż urządzenie Duffa. Powiązane, ostatnio widziałem też coś takiego switch(x) { case A: case B: do_this(); if(x == B) also_do_that(); ... }. To też, IMO, było okropne. Proszę, po prostu wypisz takie rzeczy jak instrukcje, nawet jeśli oznacza to, że musisz powtórzyć jedną linię w dwóch miejscach. Używaj funkcji i zmiennych (i dokumentacji!), Aby zmniejszyć ryzyko przypadkowej późniejszej aktualizacji tylko w jednym miejscu.
ilkkachu
50
:-) Dla tych, którzy zostali kontuzjowani lub okaleczeni w wyniku patrzenia na ten kod, nie powiedziałem, że to dobry pomysł. Właściwie powiedziałem, że to głupi pomysł.
Mark Adler
4
Zwróć uwagę, że takie konstrukcje wewnątrz przełączników NIE współgrają z RAII :(
Mooing Duck,

Odpowiedzi:

58

Tak, jest to dozwolone i robi to, co chcesz. W przypadku switchinstrukcji standard C ++ mówi :

etykiety przypadków i domyślne same w sobie nie zmieniają przepływu kontroli, która przebiega bez przeszkód na takich etykietach. Aby wyjść z przełącznika, patrz przerwa.

[Uwaga 1: Zwykle podstacja będąca przedmiotem przełączenia jest złożona, a wielkość liter i domyślne etykiety pojawiają się na instrukcjach najwyższego poziomu zawartych w (złożonym) podstacji, ale nie jest to wymagane. Deklaracje mogą pojawić się w podrzędności instrukcji switch. - notatka końcowa]

Kiedy więc ifinstrukcja jest oceniana, przepływ sterowania przebiega zgodnie z regułami ifinstrukcji, niezależnie od występujących w nim etykiet przypadków.

cigien
źródło
13
Jako konkretny przypadek można spojrzeć na Boost.Coroutines, aby znaleźć zestaw makr wywołujących ubijanie żołądka, które wykorzystują tę regułę (i pół tuzina innych narożnych przypadków C ++) do implementacji procedur używających reguł C ++ 03. Nie były częścią języka aż do C ++ 20, ale te makra sprawiły, że działały ... pod warunkiem, że najpierw wziąłeś leki zobojętniające!
Cort Ammon,
60

Tak, to powinno działać. Etykiety przypadków dla instrukcji switch w C są prawie identyczne z etykietami goto (z pewnymi zastrzeżeniami dotyczącymi ich działania z zagnieżdżonymi instrukcjami switch). W szczególności same nie definiują bloków dla instrukcji, o których myślisz, że są „wewnątrz sprawy” i możesz ich użyć, aby wskoczyć do środka bloku, tak jak w przypadku goto. Podczas przeskakiwania do środka bloku obowiązują te same zastrzeżenia, co w przypadku goto, dotyczące przeskakiwania inicjalizacji zmiennych itp.

Mając to na uwadze, w praktyce prawdopodobnie łatwiej jest napisać to za pomocą instrukcji goto, jak w:

    switch (i) {
    case 0:
        putchar('a');
        goto case2;
    case 1:
        putchar('b');
        // @fallthrough@
    case2:
    case 2:
        putchar('c');
        break;
    }
R .. GitHub PRZESTAŃ POMÓC LODOWI
źródło
1
„Podczas wskakiwania do środka bloku obowiązują te same zastrzeżenia, co w przypadku goto, dotyczące przeskakiwania inicjalizacji zmiennych itp.” Jakie to byłyby zastrzeżenia? Nie można przeskoczyć inicjalizacji zmiennych.
Asteroids With Wings
4
@AsteroidsWithWings, czy masz na myśli „nie można” z perspektywy zgodnej ze standardami, czy z praktycznej perspektywy, na którą kompilator na to nie pozwoli? Ponieważ moje GCC zezwala na to w trybie C, z ostrzeżeniem. Jednak nie pozwala na to w trybie C ++.
ilkkachu
5
@quetzalcoatl gcc-9 i clang-6 zezwalają na ten kod, ostrzegając przed potencjalnie niezainicjalizowanym kodem bee(w trybie C; w C ++ błąd „nie może przeskoczyć z instrukcji switch do tego przypadku etykieta / skok omija inicjalizację zmiennej”).
Ruslan
7
Wiesz, że robisz coś źle, gdy używasz gotojest czystszym rozwiązaniem.
Konrad Rudolph
9
@KonradRudolph: gotojest bardzo złośliwy, ale naprawdę nie ma w nim nic niewłaściwego, jeśli nie robisz z niego skomplikowanych struktur kontrolnych ręcznie. Cała „goto uważana za szkodliwą” dotyczyła pisania kodu HLL (np. C), który wygląda jak asm emitowany przez kompilator (jmps tam iz powrotem w każdym miejscu). Istnieje wiele dobrze zorganizowanych zastosowań goto, takich jak tylko do przodu (koncepcyjnie nie różni się od breaklub wczesne return), mało prawdopodobne-powtórne tylko w pętli (pozwala uniknąć zasłaniania wspólnej ścieżki przepływu i kompensuje brak zagnieżdżonych- continue) itp.
R .. GitHub STOP HELPING ICE
28

Jak wspomniały inne odpowiedzi, jest to technicznie dozwolone przez standard, ale jest to bardzo zagmatwane i niejasne dla przyszłych czytelników kodu.

Dlatego switch ... caseinstrukcje powinny być zwykle pisane z wywołaniami funkcji, a nie z dużą ilością kodu wbudowanego.

switch(i) {
case 0:
    do_zero_case(); do_general_stuff(); break;
case 1:
    do_one_case(); do_general_stuff(); break;
case 2:
    do_general_stuff(); break;
default:
    do_default_not_zero_not_one_not_general_stuff(); break;
}
Pete Becker
źródło
Zauważ, że kod w pytaniu nie jest do_general_stuffdomyślny, tylko dla przypadku 2 (oraz 0 i 1). iJednak nigdy nie uruchamia przełącznika poza zakresem 0..2.
Peter Cordes,
@PeterCordes - zwróć uwagę, że kod w odpowiedzi nie jest do_general_stuffdomyślny, tylko dla przypadku 2 (oraz 0 i 1).
Pete Becker
Sugestia - może domyślna sprawa powinna zostać usunięta lub zmieniona? Dość łatwo przeoczyć różne nazwy funkcji.
I.Am.A. Guy
@PeteBecker: Och, IDK, jak przegapiłem to generali defaultbyły to różne słowa. OTOH, to znany fakt, że ludzie zazwyczaj nadal potrafią czytać słowo, jeśli środkowe litery są zaszyfrowane; mamy tendencję do patrzenia na początek i koniec, więc posiadanie innego tylko środka prawdopodobnie nie jest idealnym rozwiązaniem dla odtłuszczania. Być może usuń do_przedrostek.
Peter Cordes
1
@supercat: Ten „dobrze znany fakt” nie polega na zamianie środkowych liter na inne, ale na pomieszaniu ich kolejności. - Jeśli cialm jest prawdziwy w graneel, nie możesz być aczkolwiek do raed tihs.
Michael Karcher