Prawidłowa, ale bezwartościowa składnia w przypadku przełączników?

207

Przez małą literówkę przypadkowo znalazłem ten konstrukt:

int main(void) {
    char foo = 'c';

    switch(foo)
    {
        printf("Cant Touch This\n");   // This line is Unreachable

        case 'a': printf("A\n"); break;
        case 'b': printf("B\n"); break;
        case 'c': printf("C\n"); break;
        case 'd': printf("D\n"); break;
    }

    return 0;
}

Wygląda printfna to, że górna część switchinstrukcji jest poprawna, ale również całkowicie nieosiągalna.

Mam czystą kompilację, nawet bez ostrzeżenia o nieosiągalnym kodzie, ale wydaje się to bezcelowe.

Czy kompilator powinien oznaczyć ten kod jako nieosiągalny?
Czy to w ogóle ma jakiś cel?

Abelenky
źródło
51
GCC ma do tego specjalną flagę. To-Wswitch-unreachable
Eli Sadoff,
57
„Czy to w ogóle ma jakiś cel?” Cóż, możesz gotowchodzić i wychodzić z nieosiągalnej w inny sposób części, co może być przydatne w przypadku różnych włamań.
HolyBlackCat
12
@HolyBlackCat Czy nie byłoby tak w przypadku całego nieosiągalnego kodu?
Eli Sadoff
28
@EliSadoff Rzeczywiście. Myślę, że to nie służy żadnemu specjalnemu celowi. Założę się, że jest to dozwolone tylko dlatego, że nie ma powodu, aby tego zabraniać. W końcu switchjest to tylko warunek gotoz wieloma etykietami. Istnieją mniej więcej takie same ograniczenia dotyczące jego ciała, jak w przypadku zwykłego bloku kodu wypełnionego etykietami goto.
HolyBlackCat
16
Warto zauważyć, że przykład @MooingDuck to wariant urządzenia Duffa ( en.wikipedia.org/wiki/Duff's_device )
Michael Anderson

Odpowiedzi:

226

Być może nie jest to najbardziej przydatne, ale nie całkowicie bezwartościowe. Możesz go użyć do zadeklarowania zmiennej lokalnej dostępnej w switchzakresie.

switch (foo)
{
    int i;
case 0:
    i = 0;
    //....
case 1:
    i = 1;
    //....
}

Standard ( N1579 6.8.4.2/7) ma następującą próbkę:

PRZYKŁAD W fragmencie sztucznego programu

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

obiekt, którego identyfikator iistnieje z automatycznym czasem przechowywania (w obrębie bloku), ale nigdy nie jest inicjowany, a zatem jeśli wyrażenie kontrolne ma niezerową wartość, wywołanie printffunkcji uzyska dostęp do nieokreślonej wartości. Podobnie fnie można nawiązać połączenia z funkcją .

PS BTW, próbka nie jest poprawnym kodem C ++. W takim przypadku ( N4140 6.7/3podkreśl moje):

Program, który przeskakuje 90 z miejsca, w którym zmienna z automatycznym czasem przechowywania nie wchodzi w zakres, do punktu, w którym jest objęty zakresem, jest źle sformułowany, chyba że zmienna ma typ skalarny, typ klasy z trywialnym domyślnym konstruktorem i trywialny destruktor, wersja jednego z tych typów kwalifikowana do cv lub tablica jednego z poprzednich typów i jest zadeklarowana bez inicjatora (8.5).


90) Przejście z warunku switchoświadczenia na etykietę sprawy uważa się za skok pod tym względem.

Więc zastąpienie int i = 4;ze int i;robi to ważny C ++.

AlexD
źródło
3
„... ale nigdy nie jest inicjowane ...” Wygląda na ito, że jest inicjowane na 4, czego mi brakuje?
yano
7
Zauważ, że jeśli zmienna jest static, zostanie ona zainicjowana na zero, więc jest to również bezpieczne użycie.
Leushenko
23
@yano Zawsze przeskakujemy i = 4;inicjalizację, więc nigdy nie ma ona miejsca.
AlexD
12
Hah oczywiście! ... cały punkt pytania ... rany. Silne jest pragnienie usunięcia tej głupoty
yano
1
Miły! Czasami potrzebowałem zmiennej temp wewnątrz casei zawsze musiałem używać różnych nazw w każdej z nich caselub definiować ją poza przełącznikiem.
SJuan76,
98

Czy to w ogóle ma jakiś cel?

Tak. Jeśli zamiast instrukcji wstawisz deklarację przed pierwszą etykietą, może to mieć sens:

switch (a) {
  int i;
case 0:
  i = f(); g(); h(i);
  break;
case 1:
  i = g(); f(); h(i);
  break;
}

Reguły dotyczące deklaracji i instrukcji są ogólnie wspólne dla bloków, więc jest to ta sama reguła, która pozwala na to, że zezwala również na instrukcje tam.


Warto również wspomnieć, że jeśli pierwsza instrukcja jest konstrukcją pętli, etykiety przypadków mogą pojawić się w treści pętli:

switch (i) {
  for (;;) {
    f();
  case 1:
    g();
  case 2:
    if (h()) break;
  }
}

Proszę nie pisać takiego kodu, jeśli istnieje bardziej czytelny sposób pisania, ale jest on całkowicie poprawny, a f()połączenie jest osiągalne.


źródło
@MatthieuM Urządzenie Duffa ma etykiety spraw wewnątrz pętli, ale zaczyna się od etykiety spraw przed pętlą.
2
Nie jestem pewien, czy powinienem głosować za interesującym przykładem, czy głosować za całkowitym szaleństwem pisania tego w prawdziwym programie :). Gratulacje za nurkowanie w otchłań i powrót w jednym kawałku.
Liviu T.
@ChemicalEngineer: Jeśli kod jest częścią pętli, podobnie jak w urządzeniu Duffa, { /*code*/ switch(x) { } }może wyglądać na czystszy, ale jest również błędny .
Ben Voigt,
39

Znane jest zastosowanie tego urządzenia o nazwie Duff's Device .

int n = (count+3)/4;
switch (count % 4) {
  do {
    case 0: *to = *from++;
    case 3: *to = *from++;
    case 2: *to = *from++;
    case 1: *to = *from++;
  } while (--n > 0);
}

Tutaj kopiujemy bufor wskazywany przez fromdo bufora wskazywanego przez to. Kopiujemy countwystąpienia danych.

do{}while()Oświadczenie zaczyna się przed pierwszym caseznacznikiem, a caseznaczniki są osadzone w do{}while().

Zmniejsza to liczbę rozgałęzień warunkowych na końcu do{}while()pętli napotkanych z grubsza o współczynnik 4 (w tym przykładzie; stałą można dostosować do dowolnej wartości).

Teraz optymalizatorzy mogą czasami zrobić to za Ciebie (szczególnie jeśli optymalizują instrukcje przesyłania strumieniowego / wektoryzacji), ale bez optymalizacji kierowanej profilem nie mogą wiedzieć, czy oczekujesz dużej pętli.

Zasadniczo mogą tam występować zmienne deklaracje i mogą być używane w każdym przypadku, ale po zakończeniu przełączania będą poza zakresem. (pamiętaj, że każda inicjalizacja zostanie pominięta)

Ponadto przepływ sterowania, który nie jest specyficzny dla przełącznika, może doprowadzić cię do tej sekcji bloku przełącznika, jak pokazano powyżej lub za pomocą goto.

Jak - Adam Nevraumont
źródło
2
Oczywiście nadal byłoby to możliwe bez dopuszczenia stwierdzeń powyżej pierwszego przypadku, ponieważ kolejność do {i case 0:nie ma znaczenia, oba służą do umieszczenia celu skoku na pierwszym *to = *from++;.
Ben Voigt,
1
@BenVoigt Twierdzę, że umieszczenie tego do {jest bardziej czytelne. Tak, kłótnia o czytelność Urządzenia Duffa jest głupia i bezcelowa i prawdopodobnie jest to prosta droga do szaleństwa.
Pozew Fund Moniki w dniu
1
@QPaysTaxes Powinieneś sprawdzić Simon Tatham za współprogram w C . Albo może nie.
Jonas Schäfer
@ JonasSchäfer Zabawne, że to w zasadzie to, co dla Ciebie zrobią kortyny C ++ 20.
Yakk - Adam Nevraumont
15

Zakładając, że używasz gcc w Linuksie, dałoby ci ostrzeżenie, jeśli używasz 4.4 lub wcześniejszej wersji.

Opcja -Wunreachable-code została usunięta w gcc 4.4 i nowszych.

16 ton
źródło
Doświadczenie tego problemu z pierwszej ręki zawsze pomaga!
16ton
@JonathanLeffler: Ogólny problem ostrzeżeń gcc, które są podatne na określony zestaw wybranych przebiegów optymalizacji, jest niestety nadal prawdziwy i sprawia, że ​​korzystanie z niego jest słabe. Naprawdę denerwujące jest posiadanie czystej wersji debugowania, a następnie nieudanej wersji wydania: /
Matthieu M.
@MatthieuM .: Wydaje się, że takie ostrzeżenie byłoby niezwykle łatwe do wykrycia w przypadkach dotyczących analizy gramatycznej, a nie semantycznej [np. Kod następuje po „jeśli”, który zwraca obie gałęzie], i ułatwiłoby stłumienie „irytacji” ostrzeżenia Z drugiej strony istnieją przypadki, w których przydatne są błędy lub ostrzeżenia w kompilacjach wersji, które nie znajdują się w kompilacjach debugowania (jeśli nic innego, w miejscach, w których hack, który jest wprowadzany w celu debugowania, powinien zostać wyczyszczony wydanie).
supercat
1
@ MatthieuM .: Gdyby wymagana była znacząca analiza semantyczna w celu wykrycia, że ​​pewna zmienna zawsze będzie fałszywa w pewnym miejscu w kodzie, kod, który jest uzależniony od tego, czy zmienna jest prawdziwa, okazałby się nieosiągalny tylko wtedy, gdyby taka analiza została przeprowadzona. Z drugiej strony rozważałbym uwagę, że taki kod był nieosiągalny raczej inaczej niż ostrzeżenie o składniowo nieosiągalnym kodzie, ponieważ całkowicie normalne jest, że możliwe są różne warunki dla niektórych konfiguracji projektu, ale nie dla innych. Czasami może być pomocny dla programistów ...
supercat
1
... wiedzieć, dlaczego niektóre konfiguracje generują większy kod niż inne [np. ponieważ kompilator może uznać jeden warunek za niemożliwy w jednej konfiguracji, ale nie w innym], ale to nie znaczy, że jest coś „złego” w kodzie, który można zoptymalizować w ta moda w niektórych konfiguracjach.
supercat
11

Nie tylko dla deklaracji zmiennych, ale także dla zaawansowanych skoków. Możesz go dobrze wykorzystać, jeśli tylko nie jesteś podatny na kod spaghetti.

int main()
{
    int i = 1;
    switch(i)
    {
        nocase:
        printf("no case\n");

        case 0: printf("0\n"); break;
        case 1: printf("1\n"); goto nocase;
    }
    return 0;
}

Wydruki

1
no case
0 /* Notice how "0" prints even though i = 1 */

Należy zauważyć, że skrzynka przełączników jest jedną z najszybszych klauzul przepływu sterowania. Musi więc być bardzo elastyczny dla programisty, co czasami obejmuje takie przypadki.

Sanchke Dellowar
źródło
A jaka jest różnica między nocase:i default:?
i486,
@ i486 Gdy i=4się nie uruchamia nocase.
Sanchke Dellowar
@SanchkeDellowar o to mi chodzi.
njzk2
Dlaczego, do cholery, miałby to zrobić, zamiast po prostu umieścić przypadek 1 przed przypadkiem 0 i zastosować przewrotność?
Jonas Schäfer
@JonasWielicki W tym celu możesz to zrobić. Ale ten kod jest tylko przykładem tego, co można zrobić.
Sanchke Dellowar
11

Należy zauważyć, że praktycznie nie ma ograniczeń strukturalnych w kodzie w switchinstrukcji ani w tym, gdzie case *:etykiety są umieszczone w tym kodzie *. Dzięki temu możliwe są sztuczki programistyczne, takie jak urządzenie duffa , których jedna możliwa implementacja wygląda następująco:

int n = ...;
int iterations = n/8;
switch(n%8) {
    while(iterations--) {
        sum += *ptr++;
        case 7: sum += *ptr++;
        case 6: sum += *ptr++;
        case 5: sum += *ptr++;
        case 4: sum += *ptr++;
        case 3: sum += *ptr++;
        case 2: sum += *ptr++;
        case 1: sum += *ptr++;
        case 0: ;
    }
}

Widzisz, kod między etykietą switch(n%8) {a case 7:etykietą jest zdecydowanie osiągalny ...


* Jak supercat na szczęście zauważył w komentarzu : Od C99 gotoani etykieta, ani etykieta (czy to case *:etykieta, czy nie) nie mogą pojawiać się w zakresie deklaracji zawierającej deklarację VLA. Nie można zatem powiedzieć, że nie ma żadnych ograniczeń strukturalnych dotyczących umieszczania case *:etykiet. Jednak urządzenie duff wyprzedza standard C99 i i tak nie zależy od VLA. Niemniej jednak czułem się zmuszony wstawić „wirtualnie” do mojego pierwszego zdania z tego powodu.

cmaster - przywróć monikę
źródło
Dodanie tablic o zmiennej długości doprowadziło do nałożenia związanych z nimi ograniczeń strukturalnych.
supercat
@supercat Jakie ograniczenia?
cmaster
1
Ani etykieta gotonor nie switch/case/defaultmoże pojawiać się w zakresie dowolnego deklarowanego obiektu lub typu. Oznacza to skutecznie, że jeśli blok zawiera jakiekolwiek deklaracje obiektów lub typów tablic o zmiennej długości, wszelkie etykiety muszą poprzedzać te deklaracje. W standardzie występuje nieco myląca wersja, która sugerowałaby, że w niektórych przypadkach zakres deklaracji VLA rozciąga się na całość instrukcji przełączania; moje pytanie na ten temat znajduje się na stackoverflow.com/questions/41752072/ ...
supercat
@ supercat: Po prostu źle zrozumiałeś ten zwrot (przypuszczam, że usunąłeś swoje pytanie). Nakłada wymóg w zakresie, w jakim można zdefiniować VLA. Nie rozszerza tego zakresu, po prostu unieważnia niektóre definicje VLA.
Keith Thompson
@KeithThompson: Tak, źle to zrozumiałem. Dziwne użycie czasu teraźniejszego w przypisie wprowadzało zamieszanie i myślę, że pojęcie to można by lepiej wyrazić jako zakaz: „Instrukcja zamiany, której treść zawiera deklarację VLA, nie będzie zawierać żadnych etykiet przełączników ani skrzynek w obrębie zakres tej deklaracji VLA ”.
supercat
10

Masz odpowiedź związana z wymaganą gccopcję -Wswitch-unreachable generowania ostrzeżenia, to odpowiedź jest opracowanie na użyteczność / worthyness części.

Cytując bezpośrednio C11, rozdział 6.8.4.2, ( moje podkreślenie )

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

obiekt, którego identyfikator iistnieje z automatycznym czasem przechowywania (w obrębie bloku), ale nigdy nie jest inicjowany , a zatem jeśli wyrażenie kontrolne ma niezerową wartość, wywołanie printf funkcji uzyska dostęp do nieokreślonej wartości. Podobnie fnie można nawiązać połączenia z funkcją .

Co jest bardzo oczywiste. Można go użyć do zdefiniowania zmiennej o zasięgu lokalnym, która jest dostępna tylko w zakresie switchinstrukcji.

Sourav Ghosh
źródło
9

Możliwe jest zaimplementowanie za nim „półtorej pętli”, chociaż może nie być to najlepszy sposób:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));
celtschk
źródło
@RichardII Czy to gra słów czy co? Proszę wytłumacz.
Dancia,
1
@Dancia Mówi, że nie jest to najlepszy sposób na zrobienie czegoś takiego, a „może nie” to niedopowiedzenie.
Pozew Fund Moniki