Оператор переключення: за замовчуванням має бути останній випадок?


178

Розглянемо наступне switchтвердження:

switch( value )
{
  case 1:
    return 1;
  default:
    value++;
    // fall-through
  case 2:
    return value * 2;
}

Цей код компілюється, але чи дійсний він (= визначена поведінка) для C90 / C99? Я ніколи не бачив код, де випадок за замовчуванням - не останній випадок.

EDIT:
Як пишуть Джон Кейдж і KillianDS : це дійсно некрасивий і заплутаний код, і я це добре знаю. Мене просто цікавить загальний синтаксис (він визначений?) Та очікуваний вихід.


19
+1 Ніколи навіть не розглядав таку поведінку
Джеймі Вонг

@ Péter Török: ти маєш на увазі, якщо значення == 2 воно поверне 6?
Олександр Ч.

4
@ Péter Török ні, порядок не має значення - якщо значення відповідає константі в будь-якому випадку мітки, то контроль перейде до цього твердження слідом за міткою, інакше контроль перейде до оператора, який слід після мітки за замовчуванням, якщо він присутній.
Піт Кіркхем

11
@Jon Cage gotoне є злим. Прихильники вантажного культу є! Ви не можете собі уявити, до яких крайнощів люди можуть уникнути, gotoоскільки це, мовляв, таке зло, що створює справжній нечитабельний безлад свого коду.
Патрік Шлютер

3
Я використовую в gotoосновному для імітації чогось на зразок finallyпункту у функціях, де ресурси (файли, пам'ять) повинні бути випущені під час зупинки та повторювати для кожного випадку помилки перелік freeі closeне допомагає читати. Хоча одне використання gotoцього я хотів би уникнути, але не можу, - це коли я хочу вирватися з циклу, і я перебуваю switchв цьому циклі.
Патрік Шлютер

Відповіді:


83

Стандарт C99 щодо цього не є явним, але, беручи всі факти разом, він цілком справедливий.

А caseта defaultмітка еквівалентні gotoмітці. Див. 6.8.1 Марковані твердження. Особливо цікавим є 6.8.1.4, який дозволяє вже згадувати пристрій Даффа:

Будь-якому оператору може передувати префікс, який оголошує ідентифікатор як ім'я мітки. Мітки самі по собі не змінюють потік контролю, який продовжує безперешкодно перетинати їх.

Редагувати : Код в комутаторі - нічого особливого; це звичайний блок коду, як у ifвикладі, з додатковими мітками стрибка. Це пояснює поведінку, що пропадає, і чому breakце необхідно.

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.

Константи регістру повинні бути унікальними в операторі комутатора:

6.8.4.2.3 Вираз кожної мітки регістру має бути цілим постійним виразом, і жодне два постійних виразу регістру у тому самому операторі вимикача не повинно мати однакове значення після перетворення. У операторі перемикання може бути не більше однієї мітки за замовчуванням.

Усі випадки оцінюються, після чого він переходить до мітки за замовчуванням, якщо вказано:

6.8.4.2.5 Цілі промоції виконуються на керуючий вираз. Постійний вираз у кожному випадку мітка перетворюється на промотований тип керуючого виразу. Якщо перетворене значення збігається з висунутим керуючим виразом, керування переходить до оператора після відповідного мітки регістру. В іншому випадку, якщо є мітка за замовчуванням, контроль переходить до мітки оператора. Якщо постійний вираз конвертованого регістру не збігається і немає мітки за замовчуванням, жодна частина тіла комутатора не виконується.


6
@HeathHunnicutt Ви чітко не зрозуміли мету прикладу. Код не складається цим плакатом, а взято прямо зі стандарту C, як ілюстрація того, як дивні заяви про перемикання та як погана практика призведе до помилок. Якби ви потурбувались прочитати текст під кодом, ви зрозуміли б стільки ж.
Лундін

2
+1, щоб компенсувати знищення. Відмовитись від когось із цитування стандарту С здається досить суворим.
Лундін

2
@Lundin Я не перебуваю під голосуванням стандартом C, і я нічого не пропустив, як ви пропонуєте. Я заперечно проголосував за погану педагогіку використання поганого і непотрібного прикладу. Зокрема, цей приклад стосується цілком іншої ситуації, ніж про неї запитували. Я міг би продовжити, але "дякую за відгуки".
Heath Hunnicutt

12
Intel пропонує вам розмістити найчастіший код спочатку в операторі перемикання підрозділу «Відділення та петля» для запобігання непередбачуваним прогнозам . Я тут, тому що у мене є defaultвипадки, що домінують в інших випадках приблизно 100: 1, і я не знаю, чи є його дійсним чи невизначеним, щоб зробити defaultперший випадок.
jww

@jww Я не впевнений, що ти маєш на увазі під Intel. Якщо ви маєте на увазі інтелект, я назву це гіпотезою. У мене було таке ж мислення, але пізніше читання стверджує, що на відміну від операторів, що перемикаються, випадковий доступ. Тож останній випадок досягти не повільніше, ніж перший. Це здійснюється шляхом хешування постійних значень випадку. Ось чому оператори перемикання швидше, ніж якщо оператори, коли гілки багато.

91

Оператори справи та оператор за замовчуванням можуть виникати в будь-якому порядку в операторі комутатора. За замовчуванням пропозиція - це необов'язкове застереження, яке відповідає, якщо жодна константа в операторах справи не може бути узгоджена.

Хороший приклад: -

switch(5) {
  case 1:
    echo "1";
    break;
  case 2:
  default:
    echo "2, default";
    break;
  case 3;
    echo "3";
    break;
}


Outputs '2,default'

дуже корисно, якщо ви хочете, щоб ваші випадки були представлені в логічному порядку в коді (як, якщо не сказати, випадок 1, випадок 3, випадок 2 / за замовчуванням) і ваші справи дуже довгі, тому ви не хочете повторювати всю справу код внизу за замовчуванням


7
Це саме той сценарій, коли я зазвичай розміщую за замовчуванням десь інше, ніж кінець ... Існує логічний порядок явних випадків (1, 2, 3), і я хочу, щоб за замовчуванням поводилося так само, як один із явних випадків, не останній.
ArtOfWarfare

51

Це дійсно і дуже корисно в деяких випадках.

Розглянемо наступний код:

switch(poll(fds, 1, 1000000)){
   default:
    // here goes the normal case : some events occured
   break;
   case 0:
    // here goes the timeout case
   break;
   case -1:
     // some error occurred, you have to check errno
}

Справа в тому, що вищезазначений код є більш читабельним та ефективним, ніж каскадний if. Можна поставити defaultнаприкінці, але це безглуздо, оскільки це зосередить вашу увагу на випадках помилок, а не на звичайних випадках (що тут і є default).

Насправді це не такий вдалий приклад, адже pollви знаєте, скільки подій може відбутися максимум. Моя реальна думка полягає в тому, що є випадки з визначеним набором вхідних значень, де є "винятки" і звичайні випадки. Якщо краще поставити винятки або звичайні випадки попереду - це питання вибору.

У галузі програмного забезпечення я думаю про ще один дуже звичний випадок: рекурсії з деякими термінальними значеннями. Якщо ви можете висловити це за допомогою перемикача, defaultбуде звичайним значенням, яке містить рекурсивний виклик та виділені елементи (окремі випадки) термінальних значень. Зазвичай не потрібно зосереджуватися на термінальних значеннях.

Ще одна причина полягає в тому, що порядок справ може змінювати поведінку складеного коду, і це має значення для виконання. Більшість компіляторів генерують скомпільований код складання в тому ж порядку, що і код, що з’являється в комутаторі. Це робить перший випадок дуже відмінним від інших: всі випадки, окрім першого, включатимуть стрибок, і це спорожнить процесорні конвеєри. Ви можете зрозуміти це, як передбачувач гілки за замовчуванням запускає перший випадок, що з’являється в комутаторі. Якщо це випадок, якщо набагато частіше, ніж інші, то у вас є дуже вагомі причини викласти це як перший випадок.

Читання коментарів - це конкретна причина, чому оригінальний плакат задав це питання після прочитання реорганізації компілятора Intel Branch Loop щодо оптимізації коду.

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


6
+1 за наведення (хорошого) прикладу без поведінкової поведінки.
KillianDS

1
... думаючи про це, хоча я не переконаний, що дефолт у верхній частині добре, тому що дуже мало людей шукають його там. Можливо, буде краще призначити повернення змінній та обробляти успіх в одній стороні if та помилки в іншій стороні із заявою case.
Джон Кейдж

@Jon: просто напиши це. Ви додаєте синтаксичний шум без будь-якої переваги читабельності. І якщо дефолт знаходиться вгорі, на це дійсно не потрібно дивитися, це дійсно очевидно (це може бути більш хитро, якщо поставити його посередині).
kriss

До речі, мені не дуже подобається синтаксис перемикача / випадку. Я дуже вважаю за краще мати змогу нанести кілька ярликів після одного випадку, а не бути зобов'язаним ставити кілька послідовних case. Що пригнічує те, що він виглядає так само як синтаксичний цукор і не порушує жодного існуючого коду, якщо він підтримується.
kriss

1
@kriss: Мені було наполовину спокусити сказати: "Я теж не програміст пітона!" :)
Андрій Грімм

16

так, це дійсно, і за деяких обставин це навіть корисно. Як правило, якщо вам це не потрібно, не робіть цього.


-1: Це пахне злом для мене. Краще було б розділити код на пару операторів перемикання.
Джон Кейдж

25
@John Cage: ставити мені -1 тут неприємно. Не я винен, що це дійсний код.
Йенс Гуведт

просто цікаво, я хотів би знати, при яких обставинах це корисно?
Саліл

1
-1 була спрямована на те, що ви стверджуєте, що вона є корисною. Я зміню його на +1, якщо ви можете надати дійсний приклад для резервного копіювання заявки.
Джон Кейдж

4
Іноді при переключенні на помилку, яку ми отримали взамін від якоїсь системної функції. Скажімо, у нас є один випадок, коли ми добре знаємо, що нам потрібно зробити чистий вихід, але цей чистий вихід може зажадати деяких рядків кодування, які ми не хочемо повторювати. Але припустимо, у нас також є багато інших екзотичних кодів помилок, з якими ми не хочемо обробляти окремо. Я б подумав просто поставити переслідування у випадку за замовчуванням і дозволити йому перейти до іншого випадку і вийти чисто. Я не кажу, що ти повинен це робити так. Це лише питання смаку.
Йенс Гуведт

8

Немає визначеного порядку в операторі комутатора. Ви можете розглядати випадки як щось на зразок названої етикетки, як gotoетикетку. На відміну від того, що люди думають тут, у випадку значення 2 мітка за замовчуванням не переходить на. Для ілюстрації на класичному прикладі, ось пристрій Даффа , який є дочіркою плаката крайнощів switch/caseу С.

send(to, from, count)
register short *to, *from;
register count;
{
  register n=(count+7)/8;
  switch(count%8){
    case 0: do{ *to = *from++;
    case 7:     *to = *from++;
    case 6:     *to = *from++;
    case 5:     *to = *from++;
    case 4:     *to = *from++;
    case 3:     *to = *from++;
    case 2:     *to = *from++;
    case 1:     *to = *from++;
            }while(--n>0);
  }
}

4
І для тих, хто не знайомий з пристроєм Даффа, цей код є абсолютно нечитабельним ...
KillianDS

7

Один із сценаріїв, де я вважаю за доцільне, щоб «за замовчуванням» знаходився десь, крім кінця виписки з справи, знаходиться у машині стану, де недійсний стан повинен скинути машину та продовжуватись так, ніби це був початковий стан. Наприклад:

перемикач (віджет_держава)
{
  за замовчуванням: / * Впав з рейок - скиньте і продовжуйте * /
    widget_state = WIDGET_START;
    /* Провалюватися */
  регістр WIDGET_START:
    ...
    перерва;
  регістр WIDGET_WHATEVER:
    ...
    перерва;
}

альтернативна домовленість, якщо недійсний стан не повинен скинути машину, але має бути легко ідентифікованим як недійсний стан:

перемикач (віджет_держава) { регістр WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); перерва; регістр WIDGET_START: ... перерва; регістр WIDGET_WHATEVER: ... перерва; за замовчуванням: widget_state = WIDGET_INVALID_STATE; /* Провалюватися */ регістр WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... зробіть все, що потрібно для встановлення "безпечного" стану }

Код в іншому місці може потім перевірити наявність (widget_state == WIDGET_INVALID_STATE) і надати будь-яку інформацію про помилки або поведінку щодо скидання стану, що здається підходящим. Наприклад, код рядка стану може відображати піктограму помилки, а параметр меню "запустити віджет", який вимкнено у більшості непрацюючих станів, може бути включений як для WIDGET_INVALID_STATE, так і для WIDGET_IDLE.


6

Зворотний зв'язок з іншим прикладом: Це може бути корисно, якщо "за замовчуванням" є несподіваним випадком, і ви хочете зареєструвати помилку, але також зробити щось розумне. Приклад з мого власного коду:

  switch (style)
  {
  default:
    MSPUB_DEBUG_MSG(("Couldn't match dash style, using solid line.\n"));
  case SOLID:
    return Dash(0, RECT_DOT);
  case DASH_SYS:
  {
    Dash ret(shapeLineWidth, dotStyle);
    ret.m_dots.push_back(Dot(1, 3 * shapeLineWidth));
    return ret;
  }
  // more cases follow
  }

5

Бувають випадки, коли ви перетворюєте ENUM в рядок або перетворюєте рядок в enum у випадку, коли ви пишете / читаєте в / з файлу.

Іноді потрібно зробити одне зі значень за замовчуванням, щоб охопити помилки, допущені вручну редагуванням файлів.

switch(textureMode)
{
case ModeTiled:
default:
    // write to a file "tiled"
    break;

case ModeStretched:
    // write to a file "stretched"
    break;
}

2

defaultУмова може бути в будь-якому місці всередині комутатора , який може існувати застереження випадку. Це не обов'язково бути останнім пунктом. Я бачив код, який поставив за замовчуванням перший пункт. Виконання case 2:виконується нормально, навіть незважаючи на те, що за замовчуванням вище.

Як тест, я помістив зразок коду у функцію, названий test(int value){}та запущений:

  printf("0=%d\n", test(0));
  printf("1=%d\n", test(1));
  printf("2=%d\n", test(2));
  printf("3=%d\n", test(3));
  printf("4=%d\n", test(4));

Вихід:

0=2
1=1
2=4
3=8
4=10

1

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

Майже напевно краще розбити ці випадки на кілька операторів комутації або менших функцій.

[редагувати] @Tristopia: Ваш приклад:

Example from UCS-2 to UTF-8 conversion 

r is the destination array, 
wc is the input wchar_t  

switch(utf8_length) 
{ 
    /* Note: code falls through cases! */ 
    case 3: r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
    case 2: r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 
    case 1: r[0] = wc;
}

було б зрозуміліше щодо його наміру (я думаю), якби воно було написане так:

if( utf8_length >= 1 )
{
    r[0] = wc;

    if( utf8_length >= 2 )
    {
        r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 

        if( utf8_length == 3 )
        {
            r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
        }
    }
}   

[edit2] @Tristopia: Ваш другий приклад, мабуть, найчистіший приклад корисного використання для подальшого перегляду:

for(i=0; s[i]; i++)
{
    switch(s[i])
    {
    case '"': 
    case '\'': 
    case '\\': 
        d[dlen++] = '\\'; 
        /* fall through */ 
    default: 
        d[dlen++] = s[i]; 
    } 
}

..але особисто я розділив розпізнавання коментарів на власну функцію:

bool isComment(char charInQuestion)
{   
    bool charIsComment = false;
    switch(charInQuestion)
    {
    case '"': 
    case '\'': 
    case '\\': 
        charIsComment = true; 
    default: 
        charIsComment = false; 
    } 
    return charIsComment;
}

for(i=0; s[i]; i++)
{
    if( isComment(s[i]) )
    {
        d[dlen++] = '\\'; 
    }
    d[dlen++] = s[i]; 
}

2
Бувають випадки, коли провалитися насправді, дуже добре.
Патрік Шлютер

Приклад від UCS-2 до UTF-8 перетворення r- це масив призначення, wcє вхідний wchar_t перемикач (utf8_length) {/ * Примітка: код потрапляє через випадки! * / випадок 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; випадок 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; випадок 1: r [0] = wc; }
Патрік Шлютер

Ось інший, рядок копіювання рядка з втечею персонажа: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Патрік Шлютер

Так, але ця процедура є однією з наших точок доступу, це був найшвидший, портативний (ми не збираємося робити спосіб збирання). У нього є лише 1 тест на будь-яку довжину UTF, ваш має 2 або навіть 3. Крім того, я цього не придумав, взяв його з BSD.
Патрік Шлютер

1
Так, було, особливо в перекладах болгарською та грецькою мовами (на Solaris SPARC) та тексті з нашою внутрішньою розміткою (що становить 3 байти UTF8). Слід визнати, що в тото це було не так багато і стало неактуальним з моменту останнього оновлення обладнання, але під час його написання це мало певну зміну.
Патрік Шлютер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.