Дійсний, але нікчемний синтаксис у випадку переключення?


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твердження є дійсним, але також абсолютно недосяжним.

Я отримав чисту компіляцію, навіть не попереджуючи про недосяжний код, але це здається безглуздим.

Чи повинен цей компілятор позначити це як недоступний код?
Це взагалі служить якійсь цілі?


51
GCC має спеціальний прапор для цього. Це-Wswitch-unreachable
Елі Садофф

57
"Це взагалі служить якійсь цілі?" Ну, ви можете gotoвходити і виходити з інакше недосяжної частини, яка може бути корисною для різних хакерів.
HolyBlackCat

12
@HolyBlackCat Хіба це не було б для всіх недосяжних кодів?
Елі Садофф

28
@EliSadoff Дійсно. Я думаю, це не служить якихось спеціальним цілям. Я думаю, що це дозволено лише тому, що немає підстав забороняти це. Зрештою, switchце просто умовна кількість 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 BTW, для зразка недійсний код C ++. У цьому випадку ( N4140 6.7/3, наголос мій):

Програма, яка стрибає 90 з точки, коли змінна з автоматичною тривалістю зберігання не перебуває в області, до точки, де вона знаходиться в області дії, неправильно формується, якщо змінна не має скалярного типу , типу класу з тривіальним конструктором за замовчуванням і тривіальним деструктором, версію одного з цих типів, кваліфіковану cv, або масив одного з попередніх типів і оголошується без ініціалізатора (8.5).


90) Перехід від умови switchзаяви до мітки справи вважається стрибком у цьому відношенні.

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


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

Правила декларацій та висловлювань поділяються на блоки загалом, тому це саме правило, яке дозволяє те, що також дозволяє висловлення там.


Варто також згадати те, що якщо перше твердження є контуром циклу, в тілі циклу можуть з'являтися мітки регістру:

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

Будь ласка, не пишіть такий код, якщо існує більш читабельний спосіб його написання, але він цілком дійсний, і f()виклик доступний.


@MatthieuM Duff's Device має мітки регістру всередині циклу, але починається з мітки регістру перед циклом.

2
Я не впевнений, чи варто мені виступати за цікавий приклад чи протистояти за повне безумство писати це в реальній програмі :). Вітаю за занурення в безодню та повернення назад в одній частині.
Лівіу Т.

@ChemicalEngineer: Якщо код є частиною циклу, як це є у пристрої Даффа, він { /*code*/ switch(x) { } }може виглядати чистіше, але це також неправильно .
Ben Voigt

39

Існує відоме використання цього під назвою пристрою Пристрій Даффа .

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 (у цьому прикладі; константа може бути налаштована на будь-яке значення, яке ви хочете).

Тепер оптимізатори можуть іноді робити це за вас (особливо якщо вони оптимізують потокові / векторизовані інструкції), але без оптимізації, керованої профілем, вони не можуть знати, чи очікуєте ви, що цикл буде великим чи ні.

Взагалі, мінливі декларації можуть виникати там і використовуватись у кожному випадку, але вийти за межі після закінчення комутатора. (зауважте, будь-яка ініціалізація буде пропущена)

Крім того, потік управління, який не залежить від комутатора, може перенести вас у той розділ блоку комутаторів, як показано вище, або з а goto.


2
Звичайно, це все-таки можливо, не допускаючи тверджень вище першого випадку, оскільки порядок do {і case 0:не має значення, обидва служать для розміщення цілі стрибка на перший *to = *from++;.
Ben Voigt

1
@BenVoigt Я б стверджував, що введення цього тексту зручніше do {для читання. Так, сперечатися щодо читабельності пристрою Даффа - дурно і безглуздо і, швидше за все, простий спосіб зійти з розуму.
Фонд позову Моніки

1
@QPaysTaxes Ви повинні перевірити Саймона Tatham в співпрограми в C . А може, ні.
Jonas Schäfer

@ JonasSchäfer Забавно, це в основному те, що C ++ 20 супротивів збираються зробити для вас.
Якк - Адам Невраумон

15

Якщо припустити, що ви використовуєте gcc в Linux, він би попередив вас, якщо ви використовуєте версію 4.4 або більш ранню версію.

Параметр -Wunreachable-code було видалено в gcc 4.4 далі.


Випробувавши проблему з перших рук, завжди допомагає!
16тон

@JonathanLeffler: Загальна проблема попередження gcc, сприйнятлива до певного набору оптимізованих пропусків, вибраних як і раніше, на жаль, правдива, і це робить для поганого досвіду користувача. Дійсно дратувати мати чисту збірку налагодження з подальшим невдалим складанням випусків: /
Matthieu M.

@MatthieuM: Здавалося б, таке попередження було б надзвичайно легко виявити у випадках, що стосуються граматичного, а не семантичного аналізу [наприклад, код слід за "якщо", який повертається в обох гілках), і полегшує задушення "роздратування" попередження. З іншого боку, є деякі випадки, коли корисно мати помилки або попередження у версії версій, які не є у налагодженнях (якщо нічого іншого, в місцях, де хак, який вводиться для налагодження, повинен бути очищений звільнення).
supercat

1
@MatthieuM: Якщо знадобиться значущий семантичний аналіз, щоб виявити, що певна змінна завжди буде помилковою на якомусь місці в коді, код, який обумовлює істинну змінну, виявиться недосяжним лише в тому випадку, якщо такий аналіз проводиться. З іншого боку, я б вважав зауваженням, що такий код був недосяжним порівняно з попередженням про синтаксично недоступний код, оскільки це може бути цілком нормально, щоб у деяких конфігураціях проекту були можливі різні умови, але не інші. Інколи це може бути корисно програмістам ...
supercat

1
... знати, чому деякі конфігурації генерують більший код, ніж інші [наприклад, тому що компілятор може вважати якусь умову неможливою в одній конфігурації, але не в іншій], але це не означає, що в коді є щось "неправильне", яке можна оптимізувати в що мода з деякими конфігураціями.
supercat

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.
Sanchke Dellowar

@SanchkeDellowar саме так я і маю на увазі.
njzk2

Чому б чорт зробив це, замість того, щоб просто поставити випадок 1 перед випадком 0 і використовувати падіння?
Йонас Шефер

@JonasWielicki У цій цілі ви могли це зробити. Але цей код є лише показом того, що можна зробити.
Sanchke Dellowar

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, ні a, gotoні етикетка (будь то case *:етикетка чи ні) не може відображатися в межах декларації, яка містить декларацію VLA. Тож неправильно говорити про відсутність структурних обмежень щодо розміщення case *:етикеток. Однак пристрій Даффа передує стандарту C99, і це все одно не залежить від VLA. Тим не менш, я відчував вимушеність вставити "практично" в своє перше речення через це.


Додавання масивів змінної довжини призвело до накладення на них структурних обмежень.
supercat

@supercat Які обмеження?
cmaster - відновити моніку

1
Ні мітка, gotoні switch/case/defaultмітка не можуть відображатися в межах будь-якого об'єкта чи типу, що є змінним. Це фактично означає, що якщо блок містить будь-які оголошення об’єктів або типів масиву змінної довжини, будь-які мітки повинні передувати цим оголошенням. У Стандарті є заплутаний біт багатослівності, який дозволяє припустити, що в деяких випадках сфера дії декларації VLA поширюється на всю операцію перемикання; див. stackoverflow.com/questions/41752072/… для мого запитання з цього приводу.
supercat

@supercat: Ви просто неправильно зрозуміли цю словесність (яку я вважаю, тому ви видалили своє запитання). Він накладає вимогу щодо сфери, в якій може бути визначений VLA. Він не розширює цю сферу дії, а лише робить певні визначення VLA недійсними.
Кіт Томпсон

@KeithThompson: Так, я неправильно зрозумів це. Дивне використання теперішнього часу у виносці робило непорозуміння, і я думаю, що це поняття можна було б краще виразити як заборона: "Заява перемикача, тіло якої містить в собі декларацію VLA, не повинно містити жодного перемикача чи міток справи в сфери дії цієї декларації VLA ".
supercat

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

@RichardII Це каламбур чи що? Будь ласка, поясніть.
Дансія

1
@Dancia Він каже, що це, очевидно, не найкращий спосіб зробити щось подібне, і "може бути" - це щось заниження.
Фонд позову Моніки
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.