Чи потрібно використовувати if (0), щоб пропустити регістр у комутаторі, який повинен працювати?


122

У мене ситуація, коли я б хотів, щоб у двох випадках у операторі перемикання C ++ обидва переходили до третього випадку. Зокрема, друга справа потрапила б до третьої справи, а перша справа також потрапила б до третьої справи, не проходячи через другу справу.

У мене була німа ідея, спробував, і це спрацювало! Я загорнув другий випадок в if (0) {... }. Це виглядає так:

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

Коли я запускаю його, я отримую бажаний результат:

0: ac
1: bc
2: c

Я спробував це як на C, так і на C ++ (обидва з clang), і він зробив те саме.

Мої запитання: чи є це дійсним C / C ++? Чи повинен він робити те, що робить?


34
Так, це дійсно і працює майже з тих самих причин, чому це робить пристрій Даффа .
dxiv

42
Зауважте, що подібний код витягне вас з будь-якого покрокового покрокового коду в середовищі, яке навіть трохи дбає про читабельність та ремонтопридатність.
Andrew Henle

16
Це жахливо. Зауважте, навіть жахливіший за пристрій Даффа. Пов’язане, я також нещодавно бачив щось подібне switch(x) { case A: case B: do_this(); if(x == B) also_do_that(); ... }. Це також було, IMO, жахливим. Будь ласка, просто випишіть подібні речі, ніби твердження, навіть якщо це означає, що вам доведеться повторити один рядок у двох місцях. Використовуйте функції та змінні (та документацію!), Щоб зменшити ризик випадкового подальшого оновлення лише в одному місці.
ilkkachu

50
:-) Для тих, хто був поранений або покалічений через перегляд цього коду, я не сказав, що це хороша ідея. Насправді я сказав, що це дурна ідея.
Mark Adler

4
Зверніть увагу, що конструкції всередині таких перемикачів НЕ грають добре з RAII :(
Mooing Duck

Відповіді:


58

Так, це дозволено, і це робить те, що ти хочеш. Для switchтвердження стандарт C ++ говорить :

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

[Примітка 1: Зазвичай підзапис, який є предметом комутатора, є складеними, а мітки регістру та за замовчуванням з’являються у висловлюваннях верхнього рівня, що містяться в (складеному) підзаявленні, але це не потрібно. Декларації можуть відображатися в підзаголовку оператора перемикання. - кінцева примітка]

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


13
Як конкретний випадок цього можна поглянути на Boost.Coroutines для набору макросів, що збивають шлунок, які використовують це правило (і півтора десятка інших кутових випадків C ++) для реалізації спільних програм за допомогою правил C ++ 03. Вони не стали частиною мови до С ++ 20, але ці макроси змусили їх працювати ... до тих пір, поки ви спочатку взяли свої антациди!
Корт Аммон

60

Так, це має спрацювати. Мітки регістру для оператора switch у C майже точно такі ж, як мітки goto (з деякими застереженнями щодо того, як вони працюють із вкладеними операторами switch). Зокрема, вони самі не визначають блоки для тверджень, які, на вашу думку, знаходяться "всередині справи", і ви можете використовувати їх, щоб перейти в середину блоку так само, як це можна зробити за допомогою goto. Під час стрибка в середину блоку застосовуються ті самі застереження, що і при goto, щодо переходу через ініціалізацію змінних тощо.

З урахуванням сказаного, на практиці, мабуть, зрозуміліше писати це за допомогою оператора goto, як у:

    switch (i) {
    case 0:
        putchar('a');
        goto case2;
    case 1:
        putchar('b');
        // @fallthrough@
    case2:
    case 2:
        putchar('c');
        break;
    }

1
"При стрибку в середину блоку застосовуються ті самі застереження, що й при переході до goto щодо переходу через ініціалізацію змінних тощо." Які це застереження? Ви не можете перестрибнути ініціалізацію змінних.
Астероїди з крилами

4
@AsteroidsWithWings, ти маєш на увазі "не можна" з точки зору, що відповідає стандартам, або з тієї практичної точки зору, яку компілятор не дозволяє? Тому що мій GCC дозволяє це в режимі C, з попередженням. Однак це не дозволяє це робити в режимі C ++.
ilkkachu

5
@quetzalcoatl gcc-9 і clang-6 дозволяють цей код, попереджаючи про потенційно неініціалізовану bee(в режимі C; в C ++ вони помиляються з "неможливо перейти з оператора переходу до цього випадку мітка / перехід обходить ініціалізацію змінної").
Руслан

7
Ви знаєте, що робите щось неправильно, використовуючи gotoце чистіше рішення.
Конрад Рудольф

9
@KonradRudolph: gotoнабагато злісний, але насправді немає нічого неприємного в цьому, якщо ви не робите з нього складних структур управління вручну. Весь "goto вважається шкідливим" стосувався написання коду HLL (наприклад, C), який виглядає як asm, що видається компілятором (стрибки туди-сюди всюди). Існує багато хороших структурованих застосувань goto, таких як тільки вперед (концептуально не відрізняється від breakабо ранній return), навряд чи повторна повторна петля (уникає затьмарення загального шляху потоку та компенсує відсутність вкладених continue) тощо
R .. GitHub STOP HELPING ICE

28

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

Ось чому switch ... caseвиписки зазвичай слід писати із викликами функцій, а не з великою кількістю вбудованого коду.

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

Зверніть увагу, що код у питанні не do_general_stuffза замовчуванням, лише для випадку 2 (та 0 та 1). iОднак він ніколи не запускає перемикач за межами діапазону 0..2.
Пітер Кордес,

@PeterCordes - зауважте, що код у відповіді не do_general_stuffза замовчуванням, лише для випадку 2 (і 0 і 1).
Pete Becker

Пропозиція - можливо, типовий регістр слід видалити або перейменувати? Досить легко пропустити різні назви функцій.
I.Am.A.Guy

@PeteBecker: О, ІДК, як я пропустив це, generalі це defaultбули різні слова. ОТО, це відомий факт, що люди, як правило, все ще можуть прочитати слово, якщо середні літери скрембовані; ми схильні дивитись на початок і кінець, тому, якщо відрізнятись лише середина, мабуть, не ідеально підходить для керованості. Можливо, видалити do_префікс.
Пітер Кордес,

1
@supercat: Цей "загальновідомий факт" полягає не в тому, щоб замінити середні літери різними, а просто в змішуванні порядку. "Якщо ціалм правдивий у гранілі, то ти, слух, будеш ненависником цих".
Michael Karcher
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.