Sintaxe válida, mas sem valor, no caso de switch?

207

Por meio de um pequeno erro de digitação, encontrei acidentalmente esse construto:

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

Parece que a printfparte superior da switchdeclaração é válida, mas também completamente inacessível.

Eu obtive uma compilação limpa, sem sequer um aviso sobre código inacessível, mas isso parece inútil.

Um compilador deve sinalizar isso como código inacessível?
Isso serve a algum propósito?

abelenky
fonte
51
O GCC tem uma bandeira especial para isso. É-Wswitch-unreachable
Eli Sadoff
57
"Isso serve a algum propósito?" Bem, você pode gotoentrar e sair da parte inacessível, que pode ser útil para vários hacks.
precisa saber é o seguinte
12
@HolyBlackCat Não seria assim para todos os códigos inacessíveis?
Eli Sadoff
28
@EliSadoff Indeed. Eu acho que não serve a nenhum propósito especial . Aposto que é permitido apenas porque não há razão para proibi-lo. Afinal, switché apenas uma condição gotocom vários rótulos. Existem mais ou menos as mesmas restrições em seu corpo que você teria em um bloco de código regular preenchido com rótulos de goto.
precisa saber é o seguinte
16
Vale ressaltar que o exemplo de @MooingDuck é uma variante do dispositivo de Duff ( en.wikipedia.org/wiki/Duff's_device ) #
Michael Anderson

Respostas:

226

Talvez não seja o mais útil, mas não completamente inútil. Você pode usá-lo para declarar uma variável local disponível no switchescopo.

switch (foo)
{
    int i;
case 0:
    i = 0;
    //....
case 1:
    i = 1;
    //....
}

O padrão ( N1579 6.8.4.2/7) tem o seguinte exemplo:

EXEMPLO No fragmento de programa artificial

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

o objeto cujo identificador iexiste com duração de armazenamento automático (dentro do bloco), mas nunca é inicializado e, portanto, se a expressão de controle tiver um valor diferente de zero, a chamada para a printffunção acessará um valor indeterminado. Da mesma forma, a chamada para a função fnão pode ser alcançada.

PS BTW, a amostra não é um código C ++ válido. Nesse caso ( N4140 6.7/3ênfase minha):

Um programa que salta 90 de um ponto em que uma variável com duração de armazenamento automático não está no escopo para um ponto em que está no escopo está mal formada , a menos que a variável tenha tipo escalar , tipo de classe com um construtor padrão trivial e um destruidor trivial, uma versão qualificada para cv de um desses tipos ou uma matriz de um dos tipos anteriores e é declarada sem um inicializador (8.5).


90) A transferência da condição de uma switchdeclaração para um rótulo de caso é considerada um salto a esse respeito.

Portanto, substituir int i = 4;por int i;torna um C ++ válido.

AlexD
fonte
3
"... mas nunca é inicializado ..." Parece que ifoi inicializado com 4, o que estou perdendo?
yano
7
Observe que, se a variável for static, ela será inicializada como zero, portanto, há um uso seguro para isso também.
Leushenko
23
@yano Sempre saltamos sobre a i = 4;inicialização, para que nunca ocorra.
precisa saber é
12
Hah, é claro! ... ponto inteiro da questão ... caramba. O desejo é forte para excluir essa estupidez #
18717
1
Agradável! Às vezes, eu precisava de uma variável temporária dentro de a casee sempre precisava usar nomes diferentes em cada uma caseou defini-la fora do comutador.
precisa saber é o seguinte
98

Isso serve a algum propósito?

Sim. Se, em vez de uma declaração, você colocar uma declaração antes do primeiro rótulo, isso pode fazer todo o sentido:

switch (a) {
  int i;
case 0:
  i = f(); g(); h(i);
  break;
case 1:
  i = g(); f(); h(i);
  break;
}

As regras para declarações e declarações são compartilhadas para blocos em geral, portanto, é a mesma regra que permite que isso também permita declarações lá.


Também vale a pena mencionar que, se a primeira instrução for uma construção de loop, os rótulos de caso poderão aparecer no corpo do loop:

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

Por favor, não escreva um código como este se houver uma maneira mais legível de escrevê-lo, mas é perfeitamente válido e a f()chamada é acessível.


fonte
O dispositivo de Duff do @MatthieuM tem rótulos de maiúsculas e minúsculas dentro de um loop, mas começa com um rótulo de maiúsculas e minúsculas antes do loop.
2
Não tenho certeza se devo votar pelo exemplo interessante ou votar pela loucura de escrever isso em um programa real :). Parabéns por mergulhar no abismo e retornar de uma só vez.
Liviu T.
@ ChemistryEngineer: se o código fizer parte de um loop, como no dispositivo de Duff, { /*code*/ switch(x) { } }pode parecer mais limpo, mas também está errado .
Ben Voigt
39

Existe um uso famoso desse dispositivo chamado Duff's Device .

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

Aqui, copiamos um buffer apontado por por frompara um buffer apontado por to. Copiamos countinstâncias de dados.

A do{}while()instrução começa antes do primeiro caserótulo e os caserótulos são incorporados no do{}while().

Isso reduz o número de ramificações condicionais no final do do{}while()loop encontrado por aproximadamente um fator de 4 (neste exemplo; a constante pode ser ajustada para qualquer valor que você desejar).

Agora, às vezes, os otimizadores podem fazer isso por você (especialmente se estão otimizando instruções de streaming / vetorizadas), mas sem a otimização guiada por perfil, eles não sabem se você espera que o loop seja grande ou não.

Em geral, as declarações de variáveis ​​podem ocorrer lá e ser usadas em todos os casos, mas ficam fora do escopo após o término da opção. (observe que qualquer inicialização será ignorada)

Além disso, o fluxo de controle que não é específico do comutador pode levá-lo àquela seção do bloco do comutador, conforme ilustrado acima, ou com a goto.

Yakk - Adam Nevraumont
fonte
2
Obviamente, isso ainda seria possível sem permitir declarações acima do primeiro caso, pois a ordem do {e o valor case 0:não importam servem para colocar um alvo de salto no primeiro *to = *from++;.
Ben Voigt
1
@BenVoigt Eu diria que colocar o texto do {é mais legível. Sim, argumentar sobre a legibilidade do dispositivo de Duff é estúpido e inútil e provavelmente uma maneira simples de enlouquecer.
Fund Monica's Lawsuit
1
@QPaysTaxes Você deve verificar se de Simon Tatham co-rotinas em C . Ou talvez não.
Jonas Schäfer
@ JonasSchäfer Curiosamente, isso é basicamente o que a C ++ 20 coroutines fará por você.
Yakk - Adam Nevraumont
15

Supondo que você esteja usando o gcc no Linux, isso seria um aviso se você estivesse usando a versão 4.4 ou anterior.

A opção -Wunreachable-code foi removida no gcc 4.4 em diante.

16tons
fonte
Ter experimentado o problema em primeira mão sempre ajuda!
16tons
@ JonathanLeffler: O problema geral de os avisos do gcc serem suscetíveis ao conjunto específico de passes de otimização selecionados ainda é verdadeiro, infelizmente, e contribui para uma má experiência do usuário. É realmente irritante ter uma compilação de depuração limpa seguida por uma compilação de versão com falha: /
Matthieu M.
@ MatthieuM .: Parece que esse aviso seria extremamente fácil de detectar em casos envolvendo análise gramatical em vez de semântica [por exemplo, o código segue um "se" que tem retornos nos dois ramos] e facilita a sufocação do "aborrecimento" avisos. Por outro lado, há alguns casos em que é útil ter erros ou avisos nas versões de lançamento que não estão nas versões de depuração (se nada mais, em lugares onde um hack que é colocado para depuração deve ser limpo para liberação).
Supercat
1
@ MatthieuM .: Se for necessária uma análise semântica significativa para descobrir que uma determinada variável sempre será falsa em algum ponto do código, o código que depende da variável ser verdadeira será encontrado inacessível apenas se essa análise for realizada. Por outro lado, consideraria um aviso de que esse código era inacessível de maneira bastante diferente de um aviso sobre código sintaticamente inacessível, já que deveria ser inteiramente normal que várias condições fossem possíveis em algumas configurações do projeto, mas não em outras.
Às
1
... para saber por que algumas configurações geram código maior que outras [por exemplo, porque um compilador pode considerar alguma condição impossível em uma configuração, mas não em outra], mas isso não significa que haja algo "errado" no código que possa ser otimizado em dessa forma com algumas configurações.
Supercat 20/17
11

Não apenas para declaração de variáveis, mas também para salto avançado. Você pode utilizá-lo bem se, e somente se, não for propenso ao código de espaguete.

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

Impressões

1
no case
0 /* Notice how "0" prints even though i = 1 */

Deve-se notar que a caixa de comutação é uma das cláusulas de fluxo de controle mais rápidas. Portanto, deve ser muito flexível para o programador, o que às vezes envolve casos como este.

Sanchke Dellowar
fonte
E qual é a diferença entre nocase:e default:?
precisa saber é
@ i486 Quando i=4não dispara nocase.
Sanchke Dellowar
@SanchkeDellowar é isso que eu quero dizer.
Njzk2
Por que diabos alguém faria isso em vez de simplesmente colocar o caso 1 antes do caso 0 e usar o fallthrough?
Jonas Schäfer
@JonasWielicki Nesse objetivo, você poderia fazer isso. Mas esse código é apenas uma vitrine do que pode ser feito.
Sanchke Dellowar 23/01
11

Deve-se observar que praticamente não há restrições estruturais no código da switchinstrução ou em onde os case *:rótulos são colocados nesse código *. Isso possibilita truques de programação como o dispositivo de duff , uma implementação possível da seguinte forma:

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

Veja bem, o código entre o switch(n%8) {e o case 7:rótulo é definitivamente acessível ...


* Como supercat apontou felizmente em um comentário : Desde C99, nem um gotonem um rótulo (seja um case *:rótulo ou não) podem aparecer no escopo de uma declaração que contenha uma declaração VLA. Portanto, não é correto dizer que não restrições estruturais na colocação das case *:etiquetas. No entanto, o dispositivo da duff é anterior ao padrão C99 e, de qualquer maneira, não depende dos VLAs. No entanto, senti-me compelido a inserir um "virtualmente" na minha primeira frase devido a isso.

cmaster - restabelece monica
fonte
A adição de matrizes de comprimento variável levou à imposição de restrições estruturais relacionadas a elas.
Supercat
@supercat Que tipo de restrições?
cmaster - restabelece monica
1
Nem um rótulo gotonem switch/case/defaultpode aparecer no escopo de qualquer objeto ou tipo declarado de forma variável. Isso significa efetivamente que, se um bloco contiver declarações de objetos ou tipos de array de comprimento variável, quaisquer rótulos deverão preceder essas declarações. Há um pouco de verborragia confusa no Padrão, o que sugere que, em alguns casos, o escopo de uma declaração VLA se estende a toda a declaração de switch; veja stackoverflow.com/questions/41752072/… para minha pergunta sobre isso.
supercat
@ supercat: Você apenas entendeu mal esse palavreado (o que eu presumo é o motivo pelo qual você excluiu sua pergunta). Ele impõe um requisito no escopo em que um VLA pode ser definido. Ele não estende esse escopo, apenas invalida determinadas definições de VLA.
Keith Thompson
@KeithThompson: Sim, eu tinha entendido errado. O uso estranho do tempo presente na nota de rodapé deixou as coisas confusas, e acho que o conceito poderia ter sido melhor expresso como uma proibição: "Uma declaração de troca cujo corpo contenha uma declaração VLA dentro dela não deve incluir nenhuma etiqueta de troca ou caixa dentro do âmbito dessa declaração VLA ".
Supercat
10

Você tem sua resposta relacionada com o exigido gccopção -Wswitch-unreachable para gerar o aviso, esta resposta é para a reflexão sobre a usabilidade / worthyness parte.

Citando diretamente o C11capítulo §6.8.4.2, ( grifo meu )

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

o objeto cujo identificador iexiste com duração de armazenamento automático (dentro do bloco), mas nunca é inicializado ; portanto, se a expressão de controle tiver um valor diferente de zero, a chamada para a printf função acessará um valor indeterminado. Da mesma forma, a chamada para a função fnão pode ser alcançada.

O que é muito auto-explicativo. Você pode usar isso para definir uma variável com escopo local que está disponível apenas dentro do switchescopo da instrução.

Sourav Ghosh
fonte
9

É possível implementar um "loop e meio" com ele, embora possa não ser a melhor maneira de fazê-lo:

char password[100];
switch(0) do
{
  printf("Invalid password, try again.\n");
default:
  read_password(password, sizeof(password));
} while (!is_valid_password(password));
celtschk
fonte
@ Richardhard É um trocadilho ou o quê? Por favor explique.
Dancia
1
@Dancia Ele está dizendo que essa claramente não é a melhor maneira de fazer algo assim, e "talvez não" seja uma indicação incompleta .
Fund Monica's Lawsuit