Допустимый, но бесполезный синтаксис в switch-case?

207

Через небольшую опечатку я случайно нашел эту конструкцию:

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

Кажется, что printfв верхней части switchутверждения является действительным, но также совершенно недоступным.

Я получил чистую компиляцию, даже без предупреждения о недоступном коде, но это кажется бессмысленным.

Должен ли компилятор пометить это как недоступный код?
Это вообще служит какой-либо цели?

abelenky
источник
51
GCC имеет специальный флаг для этого. Это-Wswitch-unreachable
Эли
57
"Это служит какой-либо цели вообще?" Ну, вы можете gotoвходить и выходить из недоступной части, которая может быть полезна для различных взломов.
HolyBlackCat
12
@HolyBlackCat Разве это не было бы таковым для всего недоступного кода?
Эли
28
@ EliSadoff Действительно. Я предполагаю, что это не служит какой-либо специальной цели. Могу поспорить, что это разрешено только потому, что нет никаких оснований запрещать это. В конце концов, switchэто просто условие gotoс несколькими метками. Его тело имеет более или менее те же ограничения, что и обычный блок кода, заполненный метками goto.
HolyBlackCat
16
Стоит отметить, что пример @MooingDuck - это вариант устройства Даффа ( en.wikipedia.org/wiki/Duff's_device )
Майкл Андерсон

Ответы:

226

Возможно, не самый полезный, но не совсем бесполезный. Вы можете использовать его для объявления локальной переменной, доступной в switchобласти видимости.

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

Стандарт ( N1579 6.8.4.2/7) имеет следующий образец:

ПРИМЕР В искусственном фрагменте программы

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

объект, идентификатор которого iсуществует с автоматическим хранением (в пределах блока), но никогда не инициализируется, и, таким образом, если управляющее выражение имеет ненулевое значение, вызов printfфункции получит доступ к неопределенному значению. Аналогичным образом, вызов функции fне может быть достигнут.

PS Кстати, образец не является допустимым кодом C ++. В этом случае ( N4140 6.7/3выделите мое):

Программа, которая переходит на 90 градусов от точки, где переменная с автоматическим продолжением хранения не находится в области видимости, до точки, где она находится в области видимости, плохо сформирована, если переменная не имеет скалярного типа , типа класса с тривиальным конструктором по умолчанию и тривиальным деструктором, cv-квалифицированная версия одного из этих типов или массив одного из предыдущих типов и объявляется без инициализатора (8.5).


90) Переход от условия switchоператора к метке кейса считается скачком в этом отношении.

Таким образом, замена int i = 4;на int i;делает его действительным C ++.

AlexD
источник
3
«... но никогда не инициализируется ...» Похоже, iинициализируется до 4, что я пропускаю?
Яно
7
Обратите внимание, что если переменная равна static, она будет инициализирована нулем, так что для этого также есть безопасное использование.
Леушенко
23
@yano Мы всегда перепрыгиваем через i = 4;инициализацию, поэтому она никогда не происходит.
AlexD
12
Ха, конечно! ... весь смысл вопроса ... блин. Желание сильно убрать эту глупость
яно
1
Ницца! Иногда мне требовалась переменная temp внутри a case, и мне всегда приходилось использовать разные имена в каждом caseили определять ее вне переключателя.
SJuan76
98

Это вообще служит какой-либо цели?

Да. Если вместо утверждения вы поместите объявление перед первым ярлыком, это может иметь смысл:

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

Правила для объявлений и операторов являются общими для блоков в целом, так что это то же правило, которое допускает это, что также позволяет использовать операторы там.


Стоит также упомянуть, что если первый оператор является конструкцией цикла, в теле цикла могут появляться метки case:

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

Пожалуйста, не пишите код, подобный этому, если есть более читаемый способ его написания, но он совершенно действителен и f()вызов доступен.


источник
@MatthieuM Устройство Даффа имеет метки регистра внутри цикла, но начинается с метки регистра перед циклом.
2
Я не уверен, стоит ли мне повышать голос за интересный пример или понижать голос за полное безумие написания этого в реальной программе :). Поздравляю с погружением в пропасть и возвращением обратно целым.
Ливиу Т.
@ChemicalEngineer: если код является частью цикла, как в устройстве Даффа, он { /*code*/ switch(x) { } }может выглядеть чище, но это также неправильно .
Бен Фойгт
39

Есть известное использование этого устройства под названием Duff's .

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

Здесь мы копируем буфер, на который указывает, fromв буфер, на который указывает to. Мы копируем countэкземпляры данных.

do{}while()Заявление начинается перед первой caseэтикеткой, а caseметки встроены внутри do{}while().

Это уменьшает количество условных переходов в конце do{}while()цикла, встречающихся примерно в 4 раза (в этом примере константу можно настроить на любое значение, которое вы хотите).

Теперь оптимизаторы могут иногда делать это для вас (особенно, если они оптимизируют потоковые / векторизованные инструкции), но без оптимизации по профилю они не могут знать, ожидаете ли вы большой цикл или нет.

В общем, объявления переменных могут встречаться там и использоваться в каждом случае, но выходить за рамки после завершения переключения. (обратите внимание, что любая инициализация будет пропущена)

Кроме того, поток управления, не зависящий от переключателя, может привести вас в этот раздел блока переключателей, как показано выше, или с помощью a goto.

Якк - Адам Невраумонт
источник
2
Конечно, это все еще было бы возможно без разрешения операторов выше первого случая, так как порядок do {и case 0:не имеет значения, оба служат для размещения цели перехода в первом *to = *from++;.
Бен Фойгт
1
@BenVoigt Я бы сказал, что размещение текста do {более читабельно. Да, спорить о читабельности устройства Даффа глупо и бессмысленно, и, вероятно, это простой способ сойти с ума.
Фонд Моника иск
1
@QPaysTaxes Вы должны проверить сопрограммы Саймона Тэтэма в Си . А может и нет.
Йонас Шефер
@ JonasSchäfer Забавно, но это в основном то, что сопрограммы C ++ 20 собираются сделать для вас.
Якк - Адам Невраумонт
15

Если вы используете gcc в Linux, вы получите предупреждение, если вы используете 4.4 или более раннюю версию.

Опция -Wunreachable-code была удалена в gcc 4.4 и более поздних версиях .

16tons
источник
Испытав проблему, из первых рук всегда помогает!
тонн
@JonathanLeffler: Общая проблема, связанная с тем, что предупреждения gcc чувствительны к конкретному набору выбранных проходов оптимизации, к сожалению, все еще остается верной и затрудняет работу пользователя. Очень неприятно иметь чистую сборку Debug с последующей неудачной сборкой Release: /
Matthieu M.
@MatthieuM .: Казалось бы, такое предупреждение было бы чрезвычайно легко обнаружить в случаях, связанных с грамматическим, а не с семантическим анализом [например, код следует «если», которое имеет возврат в обе ветви], и облегчило бы подавление «раздражения» предупреждения. С другой стороны, в некоторых случаях полезно иметь ошибки или предупреждения в сборках выпуска, которых нет в сборках отладки (если вообще ничего, в местах, где предполагается, что хак, который вставлен для отладки, очищен для релиз).
суперкат
1
@MatthieuM .: Если потребуется значительный семантический анализ, чтобы обнаружить, что определенная переменная всегда будет ложной в некоторой точке кода, то код, который зависит от того, является ли эта переменная истинной, будет признан недоступным только в том случае, если будет выполнен такой анализ. С другой стороны, я хотел бы учесть, что такой код был недоступен, а не предупреждать о синтаксически недоступном коде, поскольку было бы вполне нормально, чтобы в некоторых конфигурациях проекта были возможны различные условия, а в других - нет. Иногда это может быть полезно для программистов ...
суперкат
1
... знать, почему некоторые конфигурации генерируют больший код, чем другие [например, потому что компилятор может считать некоторые условия невозможными в одной конфигурации, но не в другой], но это не означает, что в коде есть что-то «не так», который можно оптимизировать в эта мода с некоторыми конфигурациями.
суперкат
11

Не только для объявления переменных, но и для сложных прыжков. Вы можете использовать его хорошо, если и только если вы не склонны к спагетти-коду.

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

Печать

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

Следует отметить, что распределительный шкаф является одним из самых быстрых условий потока управления. Так что он должен быть очень гибким для программиста, что иногда включает в себя такие случаи.

Санчке Деллоар
источник
А какая разница между nocase:а default:?
i486
@ i486 Когда i=4он не срабатывает nocase.
Санчке Деллоар
@SanchkeDellowar это то, что я имею в виду.
njzk2
Почему, черт возьми, можно это сделать вместо того, чтобы просто ставить случай 1 перед делом 0 и использовать падение?
Йонас Шефер
@JonasWielicki В этой цели вы могли бы сделать это. Но этот код - всего лишь пример того, что можно сделать.
Санчке Деллоар
11

Следует отметить, что практически нет структурных ограничений на код внутри switchоператора или на то, где case *:в этом коде размещены метки *. Это делает возможными трюки программирования, такие как устройство Даффа, одна из возможных реализаций которого выглядит следующим образом:

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

Видите ли, код между switch(n%8) {и case 7:меткой определенно доступен ...


* Как Supercat благодарно отметил в комментарии : Так как C99, ни , gotoни ярлык (будь этоcase *: метка или нет) не могут появляться в рамках декларации, которая содержит декларацию VLA. Поэтому неправильно говорить, что нет никаких структурных ограничений на размещение case *:этикеток. Однако устройство Даффа предшествует стандарту C99, и оно все равно не зависит от VLA. Тем не менее, я чувствовал себя обязанным вставить «виртуально» в мое первое предложение из-за этого.

cmaster - восстановить монику
источник
Добавление массивов переменной длины привело к наложению структурных ограничений, связанных с ними.
суперкат
@supercat Какие ограничения?
cmaster - восстановить
1
Ни метка, gotoни switch/case/defaultметка не могут появляться в области действия любого объекта или типа с переменной декларацией. Это фактически означает, что если блок содержит какие-либо объявления объектов или типов массива переменной длины, любые объявления должны предшествовать этим объявлениям. В Стандарте есть немного запутанного словоблудия, которое предполагает, что в некоторых случаях область действия декларации VLA распространяется на весь оператор switch; см. stackoverflow.com/questions/41752072/… мой вопрос по этому вопросу.
суперкат
@supercat: Вы просто неправильно поняли это словоблудие (я полагаю, именно поэтому вы удалили свой вопрос). Это накладывает требование на область, в которой может быть определена VLA. Он не расширяет эту область, он просто делает некоторые определения VLA недействительными.
Кит Томпсон
@KeithThompson: Да, я неправильно понял. Странное использование настоящего времени в сноске приводило в замешательство, и я думаю, что эту концепцию можно было бы лучше выразить в виде запрета: «Оператор switch, тело которого содержит объявление VLA внутри него, не должен включать никаких меток switch или case внутри объем этой декларации VLA ".
суперкат
10

Вы получили свой ответ, связанный с необходимой gccопцией -Wswitch-unreachable для генерации предупреждения, этот ответ предназначен для более подробной информации о юзабилити / достоинстве .

Цитировать прямо из C11главы §6.8.4.2 ( выделено мной )

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

объект, идентификатор которого iсуществует с автоматическим хранением (в пределах блока), но никогда не инициализируется , и, таким образом, если управляющее выражение имеет ненулевое значение, вызов printf функции получит доступ к неопределенному значению. Аналогичным образом, вызов функции fне может быть достигнут.

Что очень очевидно. Вы можете использовать это, чтобы определить локальную переменную, которая доступна только в пределах switchоператора.

Сурав Гош
источник
9

Можно реализовать «цикл с половиной» с ним, хотя это может быть не лучшим способом сделать это:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));
celtschk
источник
@RichardII Это игра слов или что? Пожалуйста, объясни.
Dancia
1
@Dancia Он говорит, что это явно не лучший способ сделать что-то подобное, и «не может» - это что-то вроде занижения.
Фонд Моника иск