Instrução Switch: o padrão deve ser o último caso?


178

Considere a seguinte switchdeclaração:

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

Esse código é compilado, mas é válido (= comportamento definido) para C90 / C99? Nunca vi código em que o caso padrão não seja o último.

Edição:
Como Jon Cage e KillianDS escrevem: este é um código realmente feio e confuso e eu estou ciente disso. Estou interessado apenas na sintaxe geral (está definida?) E na saída esperada.


19
+1 Nunca considerou esse comportamento #
Jamie Wong

@ Péter Török: você quer dizer se o valor == 2 retornará 6?
Alexandre C.

4
@ Péter Török não, a ordem não importa - se o valor corresponder à constante em qualquer rótulo de caso, o controle irá pular para essa instrução após o rótulo; caso contrário, o controle pulará para a declaração após o rótulo padrão, se presente.
precisa

11
@ Jon Cage gotonão é mau. Seguidores de culto de carga são! Você não pode imaginar até que ponto as pessoas extremos podem evitar, gotoporque é supostamente tão ruim, criando uma verdadeira bagunça ilegível de seu código.
Patrick Schlüter

3
Uso gotoprincipalmente para simular algo como uma finallycláusula em funções, em que recursos (arquivos, memória) precisam ser liberados ao parar e repetir para cada caso de erro uma lista freee closenão ajuda na legibilidade. Há um uso gotoque eu gostaria de evitar, mas não posso, é quando eu quero sair de um loop e estou dentro de um switchloop.
Patrick Schlüter

Respostas:


83

O padrão C99 não é explícito sobre isso, mas, considerando todos os fatos, é perfeitamente válido.

A casee defaultlabel são equivalentes a um gotorótulo. Consulte 6.8.1 Instruções rotuladas. Especialmente interessante é o 6.8.1.4, que permite o já mencionado dispositivo de Duff:

Qualquer declaração pode ser precedida por um prefixo que declara um identificador como um nome de rótulo. Os rótulos em si não alteram o fluxo de controle, que continua desimpedido através deles.

Edit : O código dentro de um switch não é nada de especial; é um bloco de código normal como em uma ifdeclaração, com rótulos de salto adicionais. Isso explica o comportamento direto e por que breaké necessário.

6.8.4.2.7 até dá um exemplo:

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

No fragmento de programa artificial, o objeto cujo identificador é i existe com duração automática de armazenamento (dentro do bloco), mas nunca é inicializado; portanto, se a expressão de controle tiver um valor diferente de zero, a chamada para a função printf acessará um valor indeterminado. Da mesma forma, a chamada para a função f não pode ser alcançada.

As constantes de caso devem ser exclusivas em uma instrução switch:

6.8.4.2.3 A expressão de cada rótulo de caso deve ser uma expressão constante inteira e não duas das expressões constantes de caso na mesma instrução switch devem ter o mesmo valor após a conversão. Pode haver no máximo um rótulo padrão em uma instrução switch.

Todos os casos são avaliados e, em seguida, passam para o rótulo padrão, se fornecido:

6.8.4.2.5 As promoções de números inteiros são realizadas na expressão de controle. A expressão constante em cada rótulo de caso é convertida no tipo promovido da expressão de controle. Se um valor convertido corresponder ao da expressão de controle promovida, o controle saltará para a instrução após o rótulo da caixa correspondente. Caso contrário, se houver um rótulo padrão, o controle pula para a instrução rotulada. Se nenhuma expressão constante de maiúscula convertida corresponder e não houver rótulo padrão, nenhuma parte do corpo do comutador será executada.


6
@HeathHunnicutt Você claramente não entendeu o objetivo do exemplo. O código não é composto por este pôster, mas é retirado diretamente do padrão C, como uma ilustração de quão estranhas são as declarações de switch e de como as práticas inadequadas levam a erros. Se você se desse ao trabalho de ler o texto abaixo do código, perceberia isso.
Lundin

2
+1 para compensar o voto negativo. Fazer o voto negativo de alguém por citar o padrão C parece bastante duro.
Lundin

2
@Lundin Eu não estou fazendo voto negativo no padrão C e não ignorei nada, como você sugere. Eu rejeitei a má pedagogia de usar um exemplo ruim e desnecessário. Em particular, esse exemplo refere-se a uma situação completamente diferente da que foi questionada. Eu poderia continuar, mas "obrigado pelo seu feedback".
precisa

12
A Intel diz para você colocar o código mais frequente primeiro em uma declaração de switch na Reorganização de ramificações e laços para evitar erros de impressão . Estou aqui porque tenho um defaultcaso que domina outros casos em cerca de 100: 1 e não sei se é válido ou indefinido fazer defaulto primeiro caso.
JWW

@jww Não sei ao certo o que você quer dizer com Intel. Se você quer dizer inteligência, chamarei de hipótese. Eu tinha o mesmo pensamento, mas a leitura posterior afirma que, ao contrário das instruções if, as instruções switch são de acesso aleatório. Portanto, o último caso não é mais lento do que o primeiro. Isso é realizado através da hash dos valores constantes dos casos. É por isso que as instruções switch são mais rápidas do que as instruções if quando as ramificações são muito.

91

As instruções de caso e a instrução padrão podem ocorrer em qualquer ordem na instrução switch. A cláusula padrão é uma cláusula opcional que corresponde se nenhuma das constantes nas instruções de caso puder ser correspondida.

Bom exemplo :-

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


Outputs '2,default'

muito útil se você deseja que seus casos sejam apresentados em uma ordem lógica no código (como em não dizer caso 1, caso 3, caso 2 / padrão) e seus casos são muito longos, portanto, você não deseja repetir o caso inteiro código na parte inferior para o padrão


7
Este é exatamente o cenário em que geralmente coloco o padrão em algum lugar que não seja o final ... há uma ordem lógica para os casos explícitos (1, 2, 3) e quero que o padrão se comporte exatamente da mesma maneira que um dos casos explícitos que não é o último.
ArtOfWarfare

51

É válido e muito útil em alguns casos.

Considere o seguinte código:

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
}

O ponto é que o código acima é mais legível e eficiente do que em cascata if. Você pode colocar defaultno final, mas é inútil, pois concentrará sua atenção em casos de erro em vez de casos normais (que é o defaultcaso aqui ).

Na verdade, não é um exemplo tão bom, pois pollvocê sabe quantos eventos podem ocorrer no máximo. Meu verdadeiro ponto é que não são casos com um conjunto definido de valores de entrada, onde há 'exceções' e casos normais. Se é melhor colocar exceções ou casos normais na frente, é uma questão de escolha.

No campo do software, penso em outro caso muito comum: recursões com alguns valores terminais. Se você pode expressá-lo usando um comutador, defaultserá o valor usual que contém chamada recursiva e elementos distintos (casos individuais) dos valores do terminal. Geralmente, não há necessidade de se concentrar nos valores dos terminais.

Outro motivo é que a ordem dos casos pode alterar o comportamento do código compilado, e isso é importante para as performances. A maioria dos compiladores gerará código de montagem compilado na mesma ordem em que o código aparece no comutador. Isso torna o primeiro caso muito diferente dos outros: todos os casos, exceto o primeiro, envolvem um salto e esvaziam os pipelines do processador. Você pode entendê-lo como preditor de ramificação por padrão para executar o primeiro caso que aparece no comutador. Se um caso é muito mais comum que os outros, você tem boas razões para colocá-lo como o primeiro caso.

A leitura dos comentários é a razão específica pela qual o pôster original fez essa pergunta depois de ler a reorganização do Branch Loop do compilador Intel sobre otimização de código.

Então se tornará uma arbitragem entre a legibilidade e o desempenho do código. Provavelmente é melhor colocar um comentário para explicar ao futuro leitor por que um caso aparece primeiro.


6
+1 por dar um (bom) exemplo sem o comportamento inovador.
KillianDS

1
... pensando nisso, porém, não estou convencido de que o padrão no topo seja bom, porque poucas pessoas o procurariam lá. Talvez seja melhor atribuir o retorno a uma variável e lidar com o sucesso em um lado de um if e os erros no outro lado com uma instrução de caso.
Jon gaiola

@ Jon: basta escrevê-lo. Você adiciona ruído sintaxe sem nenhum benefício de legibilidade. E, se o padrão estiver no topo, não há realmente necessidade de olhar para ele, é realmente óbvio (pode ser mais complicado se você colocá-lo no meio).
kriss

A propósito, eu realmente não gosto da sintaxe do comutador C / caso. Eu preferiria poder colocar vários rótulos após um caso, em vez de ser obrigado a colocar vários rótulos sucessivos case. O que é deprimente é que parece com açúcar sintático e não quebra nenhum código existente, se suportado.
kriss

1
@kriss: Fiquei meio tentado a dizer "Eu também não sou um programador de python!" :)
Andrew Grimm

16

Sim, isso é válido e, em algumas circunstâncias, é útil. Geralmente, se você não precisar, não faça.


-1: Isso cheira a mal para mim. Seria melhor dividir o código em um par de instruções de chave.
Jon gaiola

25
@ John Cage: me colocar um -1 aqui é desagradável. Não é minha culpa que esse seja um código válido.
Jens Gustedt

apenas curioso, gostaria de saber em que circunstâncias é útil?
Salil

1
O -1 foi destinado à sua afirmação de que ele é útil. Vou mudar para +1 se você puder fornecer um exemplo válido para fazer backup de sua reivindicação.
Jon gaiola

4
Às vezes, ao mudar para um erro que recebemos em troca de alguma função do sistema. Digamos que temos um caso em que sabemos de maneira definitiva que precisamos fazer uma saída limpa, mas essa saída limpa pode exigir algumas linhas de codificação que não queremos repetir. Mas suponha que também tenhamos muitos outros códigos de erro exóticos que não queremos manipular individualmente. Eu consideraria apenas colocar um perror no caso padrão e deixá-lo passar para o outro caso e sair corretamente. Eu não digo que você deveria fazer assim. É apenas uma questão de gosto.
Jens Gustedt

8

Não há ordem definida em uma instrução switch. Você pode considerar os casos como algo como um rótulo nomeado, como um gotorótulo. Ao contrário do que as pessoas parecem pensar aqui, no caso do valor 2 o rótulo padrão não é utilizado. Para ilustrar com um exemplo clássico, aqui está o dispositivo de Duff , que é o filho dos extremos de switch/caseC.

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
E para quem não está familiarizado com o dispositivo de Duff este código é completamente ilegível ...
KillianDS

7

Um cenário em que eu consideraria apropriado ter um 'padrão' localizado em algum lugar que não seja o final de uma instrução de caso está em uma máquina de estado em que um estado inválido deve redefinir a máquina e continuar como se fosse o estado inicial. Por exemplo:

switch (widget_state)
{
  padrão: / * Caiu nos trilhos - redefina e continue * /
    widget_state = WIDGET_START;
    /* Cair em */
  caso WIDGET_START:
    ...
    pausa;
  caso WIDGET_WHATEVER:
    ...
    pausa;
}

um arranjo alternativo, se um estado inválido não deve reiniciar a máquina, mas deve ser facilmente identificável como um estado inválido:

switch (widget_state) { caso WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); pausa; caso WIDGET_START: ... pausa; caso WIDGET_WHATEVER: ... pausa; padrão: widget_state = WIDGET_INVALID_STATE; /* Cair em */ caso WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... faça o que for necessário para estabelecer uma condição "segura" }

O código em outro local pode então procurar por (widget_state == WIDGET_INVALID_STATE) e fornecer qualquer comportamento de relatório de erros ou redefinição de estado que pareça apropriado. Por exemplo, o código da barra de status pode mostrar um ícone de erro e a opção de menu "Iniciar widget", que está desativada na maioria dos estados não ociosos, pode ser ativada para WIDGET_INVALID_STATE e WIDGET_IDLE.


6

Discutindo com outro exemplo: Isso pode ser útil se "padrão" for um caso inesperado e você desejar registrar o erro, mas também fazer algo sensato. Exemplo de alguns dos meus próprios códigos:

  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

Há casos em que você está convertendo ENUM em uma string ou convertendo em enum no caso de estar gravando / lendo em / de um arquivo.

Às vezes, você precisa tornar um dos valores padrão para cobrir erros cometidos pela edição manual de arquivos.

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

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

2

A defaultcondição pode estar em qualquer lugar dentro do comutador em que uma cláusula de caso possa existir. Não é necessário que seja a última cláusula. Eu vi o código que coloca o padrão como a primeira cláusula. O case 2:é executado normalmente, mesmo que a cláusula padrão esteja acima dele.

Como teste, coloquei o código de exemplo em uma função, chamada test(int value){}e executada:

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

A saída é:

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

1

É válido, mas bastante desagradável. Eu sugeriria que geralmente é ruim permitir falhas, pois isso pode levar a um código de espaguete muito confuso.

É quase certamente melhor dividir esses casos em várias instruções de chave ou em funções menores.

[edit] @Tristopia: Seu exemplo:

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

seria mais claro quanto à sua intenção (eu acho) se fosse escrito assim:

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: Seu segundo exemplo é provavelmente o exemplo mais limpo de um bom uso para acompanhamento:

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

..mas pessoalmente eu dividiria o reconhecimento de comentários em sua própria função:

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
Há casos em que a queda é realmente uma boa ideia.
Patrick Schlüter

Exemplo de conversão de UCS-2 para UTF-8 ré a matriz de destino, wcé o wchar_t comutador de entrada (utf8_length) {/ * Nota: o código ocorre entre os casos! * / caso 3: r [2] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0x800; caso 2: r [1] = 0x80 | (wc & 0x3f); wc >> = 6; wc | = 0xc0; caso 1: r [0] = wc; }
Patrick Schlüter

Aqui outra, uma rotina de cópia de string com caracteres escapar: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter

Sim, mas essa rotina é um dos nossos pontos de acesso, essa foi a maneira mais rápida e portátil (não faremos montagem) de implementá-la. Ele tem apenas 1 teste para qualquer comprimento UTF, o seu tem 2 ou até 3. Além disso, eu não o criei, peguei no BSD.
Patrick Schlüter

1
Sim, houve especialmente em conversões em búlgaro e grego (no Solaris SPARC) e em texto com nossa marcação interna (que é UTF8 de 3 bytes). Admitimos que, no total, não é muito e se tornou irrelevante desde a nossa última atualização de hardware, mas, no momento em que foi escrito, fazia alguma diferença.
Patrick Schlüter
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.