Переключатель: по умолчанию должен быть последний случай?

178

Рассмотрим следующее switchутверждение:

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

Этот код компилируется, но действительно ли он (= определенное поведение) для C90 / C99? Я никогда не видел код, в котором регистр по умолчанию не последний.

РЕДАКТИРОВАТЬ:
Как пишут Джон Кейдж и KillianDS : это действительно уродливый и запутанный код, и я хорошо это знаю. Меня просто интересует общий синтаксис (он определен?) И ожидаемый результат.

tanascius
источник
19
+1 Никогда даже не задумывался над таким поведением
Джейми Вонг
@ Péter Török: вы имеете в виду, что если значение == 2, оно вернет 6?
Александр С.
4
@ Péter Török нет, порядок не имеет значения - если значение соответствует константе в какой-либо метке, то элемент управления перейдет к этому оператору после метки, в противном случае элемент управления перейдет к инструкции после метки по умолчанию, если она присутствует.
Пит Киркхам
11
@ Джон Кейдж gotoне злой. Культовые последователи груза есть! Вы не могли себе представить, к чему могут пойти люди, избегая крайностей, gotoпотому что они, как утверждается, настолько злы, что создают непонятный беспорядок в их коде.
Патрик Шлютер
3
Я использую в gotoосновном для имитации что - то вроде finallyпункта в функциях, где RESSOURCES (файлы, память) должны быть освобождены при остановке, и повторять для каждого случая ошибки списка 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 не может быть достигнут.

Константы регистра должны быть уникальными в операторе switch:

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

Все случаи оцениваются, затем он переходит к метке по умолчанию, если дано:

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

Безопасный
источник
6
@HeathHunnicutt Вы явно не поняли цель примера. Код не составлен этим плакатом, а взят прямо из стандарта C, как иллюстрация того, насколько странны операторы switch и как плохая практика приведет к ошибкам. Если бы вам надоело читать текст под кодом, вы бы поняли так же.
Лундин
2
+1, чтобы компенсировать понижение голосов. Снижение голосов со стороны кого-то из цитирования стандарта C кажется довольно резким.
Лундин
2
@ Лундин Я не голосую по стандарту С и ничего не пропустил, как вы предлагаете. Я отказался от плохой педагогики использования плохого и ненужного примера. В частности, этот пример относится к совершенно иной ситуации, чем это было задано. Я мог бы продолжить, но «спасибо за ваш отзыв».
Хит Ханникутт
12
Корпорация Intel предлагает вам разместить наиболее часто встречающийся код первым в операторе switch в Branch and Loop Reorganization для предотвращения ошибочных прогнозов . Я здесь, потому что у меня есть defaultдело, доминирующее над другими делами примерно на 100: 1, и я не знаю, является ли оно действительным или неопределенным, чтобы сделать defaultпервое дело.
jww
@jww Я не уверен, что вы подразумеваете под Intel. Если вы имеете в виду интеллект, я назову это гипотезой. У меня было такое же мнение, но в последующем чтении говорится, что в отличие от операторов if операторы switch имеют произвольный доступ. Таким образом, последний случай не медленнее, чем первый. Это достигается путем хеширования постоянных значений регистра. Вот почему операторы switch быстрее, чем операторы if, когда веток много.
91

Операторы case и инструкция по умолчанию могут встречаться в любом порядке в операторе switch. Предложение по умолчанию является необязательным предложением, которое сопоставляется, если ни одна из констант в операторах case не может быть сопоставлена.

Хороший пример :-

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 / по умолчанию) и ваши кейсы очень длинные, поэтому вы не хотите повторять весь кейс код внизу по умолчанию

Salil
источник
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будет обычное значение, которое содержит рекурсивный вызов и выделенные элементы (отдельные случаи) значения терминала. Обычно нет необходимости фокусироваться на терминальных значениях.

Другая причина в том, что порядок дел может изменить поведение скомпилированного кода, и это имеет значение для производительности. Большинство компиляторов генерируют скомпилированный ассемблерный код в том же порядке, в котором он отображается в коммутаторе. Это сильно отличает первый случай от других: все случаи, кроме первого, будут включать скачок, который опустошит конвейеры процессора. Вы можете понять это как предиктор ветвления, по умолчанию запускающий первый появившийся случай в коммутаторе. Если случай гораздо более распространенный, чем у других, у вас есть очень веские основания для того, чтобы поставить его в качестве первого случая.

Чтение комментариев - это особая причина, по которой оригинальный автор задал этот вопрос после прочтения реорганизации компилятора Branch Branch Loop об оптимизации кода.

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

Kriss
источник
6
+1 за (хороший) пример без падающего поведения.
KillianDS
1
... думая об этом, я не уверен, что наличие дефолта наверху хорошо, потому что очень немногие будут искать его там. Возможно, было бы лучше назначить возврат переменной и обработать успешное выполнение с одной стороны оператора if и ошибки с другой стороны с помощью оператора case.
Джон Кейдж
@Jon: просто напиши это. Вы добавляете синтаксический шум без какого-либо улучшения читабельности. И, если по умолчанию наверху, на самом деле нет необходимости смотреть на это, это действительно очевидно (это может быть более сложно, если вы положите его в середине).
Крис
Кстати, мне не очень нравится синтаксис C / case. Я бы предпочел иметь возможность наносить несколько ярлыков за одним делом вместо того, чтобы быть вынужденным наносить несколько последовательных case. Что удручает, так это то, что он выглядит так же, как синтаксический сахар, и не нарушает существующий код, если поддерживается.
Крис
1
@kriss: я наполовину испытывал желание сказать: «Я тоже не программист на python!» :)
Андрей Гримм
16

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

Йенс Гастедт
источник
-1: это пахнет злом для меня. Было бы лучше разделить код на пару операторов switch.
Джон Кейдж
25
@ Джон Кейдж: ставить мне -1 здесь противно. Я не виноват, что это правильный код.
Йенс Гастедт
просто любопытно, хотелось бы узнать, при каких обстоятельствах это полезно?
Салил
1
-1 был нацелен на ваше утверждение о его полезности. Я изменю его на +1, если вы сможете предоставить действительный пример для подтверждения своей заявки.
Джон Кейдж
4
Иногда при переключении на ошибку, которую мы получили взамен какой-то системной функции. Скажем, у нас есть один случай, когда мы навсегда знаем, что мы должны сделать чистый выход, но этот чистый выход может потребовать некоторых строк кода, которые мы не хотим повторять. Но предположим, что у нас также есть много других экзотических кодов ошибок, которые мы не хотим обрабатывать индивидуально. Я хотел бы рассмотреть возможность установки perror в случае по умолчанию и позволить ему перейти в другой случай и завершить работу корректно. Я не говорю, что ты должен делать это так. Это просто вопрос вкуса.
Йенс Гастедт
8

Там нет определенного порядка в инструкции switch. Вы можете рассматривать случаи как что-то вроде именованного ярлыка, как gotoярлык. Вопреки тому, что люди думают здесь, в случае значения 2 метка по умолчанию не используется. Чтобы проиллюстрировать на классическом примере, вот устройство Даффа , которое является потомком детей крайностей switch/caseв C.

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

Один сценарий, в котором я бы посчитал целесообразным расположить «по умолчанию» где-то, кроме конца оператора case, - это конечный автомат, где недопустимое состояние должно сбрасывать машину и действовать, как если бы это было начальное состояние. Например:

Переключатель (widget_state)
{
  по умолчанию: / * отвали рельсы - сброс и продолжение * /
    widget_state = WIDGET_START;
    /* Проваливаться */
  case WIDGET_START:
    ...
    перерыв;
  case WIDGET_WHATEVER:
    ...
    перерыв;
}

альтернативное расположение, если недопустимое состояние не должно сбрасывать машину, но должно быть легко идентифицировано как недопустимое состояние:

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

Затем код в другом месте может проверять наличие (widget_state == WIDGET_INVALID_STATE) и предоставлять любое сообщение об ошибке или состояние сброса состояния, которое кажется подходящим. Например, в коде строки состояния может отображаться значок ошибки, а параметр меню «Запуск виджета», который отключен в большинстве неактивных состояний, может быть включен как для WIDGET_INVALID_STATE, так и для WIDGET_IDLE.

Supercat
источник
6

Включение в другой пример: это может быть полезно, если «default» - это неожиданный случай, и вы хотите записать ошибку, но также сделать что-то разумное. Пример из моего собственного кода:

  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

Это действительно, но довольно противно. Я бы предположил, что, как правило, нельзя допускать сбои, так как это может привести к очень грязному коду спагетти.

Почти наверняка лучше разбить эти случаи на несколько операторов switch или более мелких функций.

[править] @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 | (туалет & 0x3f); wc >> = 6; wc | = 0x800; случай 2: r [1] = 0x80 | (туалет & 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). Признаюсь, что в общем-то это не так уж много и стало неактуальным с момента нашего последнего обновления оборудования, но на момент его написания это имело некоторое значение.
Патрик Шлютер