¿Se supone que funciona el uso de if (0) para omitir un caso en un interruptor?

110

Tengo una situación en la que me gustaría que dos casos en una instrucción de cambio de C ++ cayeran en un tercer caso. Específicamente, el segundo caso pasaría al tercer caso, y el primer caso también pasaría al tercer caso sin pasar por el segundo caso.

Tuve una idea tonta, la probé y ¡funcionó! Envolví el segundo caso en un if (0) {... }. Se parece a esto:

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

Cuando lo ejecuto, obtengo el resultado deseado:

0: ac
1: bc
2: c

Lo probé tanto en C como en C ++ (ambos con clang) e hizo lo mismo.

Mis preguntas son: ¿Es esto válido C / C ++? ¿Se supone que debe hacer lo que hace?

Mark Adler
fuente
33
Sí, esto es válido y funciona prácticamente por las mismas razones por las que lo hace el dispositivo de Duff .
dxiv
41
Tenga en cuenta que un código como este lo sacará de cualquier guía de código en un entorno que se preocupa incluso un poco por la legibilidad y el mantenimiento.
Andrew Henle
15
Esto es horrible. Incluso más horrible que el dispositivo de Duff, fíjate. Relacionado, también vi recientemente algo como switch(x) { case A: case B: do_this(); if(x == B) also_do_that(); ... }. Eso también fue, en mi opinión, horrible. Por favor, simplemente escriba cosas como esas como declaraciones, incluso si eso significa que tiene que repetir una línea en dos lugares. Utilice funciones y variables (¡y documentación!) Para reducir el riesgo de actualizaciones posteriores accidentales en un solo lugar.
ilkkachu
46
:-) Para aquellos que resultaron heridos o mutilados por mirar ese código, no dije que fuera una buena idea. De hecho, dije que era una idea tonta.
Mark Adler
4
Tenga en cuenta que las construcciones dentro de interruptores como este NO funcionan bien con RAII :(
Mooing Duck

Respuestas:

53

Sí, esto está permitido y hace lo que quiere. Para una switchdeclaración, el estándar C ++ dice :

las etiquetas de mayúsculas y minúsculas en sí mismas no alteran el flujo de control, que continúa sin obstáculos a través de dichas etiquetas. Para salir de un interruptor, vea romper.

[Nota 1: Por lo general, la subdeclaración que es objeto de un cambio es compuesta y las etiquetas de caso y defecto aparecen en las declaraciones de nivel superior contenidas dentro de la subdeclaración (compuesta), pero esto no es obligatorio. Las declaraciones pueden aparecer en la subenunciación de una declaración de cambio. - nota final]

Entonces, cuando ifse evalúa la declaración, el flujo de control procede de acuerdo con las reglas de una ifdeclaración, independientemente de las etiquetas de los casos que intervengan.

cigien
fuente
13
Como un caso específico de esto, uno puede mirar Boost.Coroutines para un conjunto de macros que revuelven el estómago que aprovechan esta regla (y media docena de otros casos de esquina de C ++) para implementar corrutinas usando reglas C ++ 03. No se hicieron parte del lenguaje hasta C ++ 20, pero estas macros los hicieron funcionar ... ¡siempre que tomaras tus antiácidos primero!
Cort Ammon
56

Sí, se supone que esto funciona. Las etiquetas de caso de una instrucción switch en C son casi exactamente como etiquetas goto (con algunas advertencias sobre cómo funcionan con instrucciones switch anidadas). En particular, ellos mismos no definen bloques para las declaraciones que usted considera que están "dentro del caso", y puede usarlos para saltar al medio de un bloque como lo haría con un goto. Al saltar a la mitad de un bloque, se aplican las mismas advertencias que con goto con respecto al salto sobre la inicialización de variables, etc.

Dicho esto, en la práctica probablemente sea más claro escribir esto con una declaración goto, como en:

    switch (i) {
    case 0:
        putchar('a');
        goto case2;
    case 1:
        putchar('b');
        // @fallthrough@
    case2:
    case 2:
        putchar('c');
        break;
    }
R .. GitHub DEJA DE AYUDAR A ICE
fuente
1
"Al saltar a la mitad de un bloque, se aplican las mismas advertencias que con goto con respecto al salto sobre la inicialización de variables, etc." ¿Qué advertencias serían esas? No puede saltar la inicialización de variables.
Asteroides con alas
4
@AsteroidsWithWings, ¿quiere decir "no puedo" desde una perspectiva de conformidad con los estándares, o desde el tipo de perspectiva práctica que un compilador no lo permitirá? Porque mi GCC lo permite en modo C, con una advertencia. Sin embargo, no lo permite en modo C ++.
ilkkachu
5
@quetzalcoatl gcc-9 y clang-6 permiten este código, advirtiendo sobre la posibilidad de no inicializar bee(en modo C; en C ++ el error "no se puede saltar de la declaración de cambio a esta etiqueta de caso / salto omite la inicialización de la variable").
Ruslan
7
Sabes que estás haciendo algo mal cuando usas gotoes la solución más limpia.
Konrad Rudolph
9
@KonradRudolph: gotoes muy difamado, pero en realidad no tiene nada de objetable si no está creando estructuras de control complejas manualmente. Todo el "goto considerado dañino" se trataba de escribir código HLL (por ejemplo, C) que se parece a un código emitido por un compilador (salta de un lado a otro por todas partes). Hay muchos buenos usos estructurados de goto como solo hacia adelante (conceptualmente no es diferente breako temprano return), improbable-reintento-solo-bucle (evita oscurecer la ruta de flujo común y compensa la falta de anidado- continue), etc.
R .. GitHub DEJA DE AYUDAR A ICE
28

Como han mencionado otras respuestas, esto está técnicamente permitido por el estándar, pero es muy confuso y poco claro para los futuros lectores del código.

Esta es la razón por la que las switch ... casedeclaraciones generalmente deben escribirse con llamadas a funciones y no con mucho código en línea.

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;
}
Pete Becker
fuente
Tenga en cuenta que el código de la pregunta no es do_general_stuffpredeterminado, solo para el caso 2 (y 0 y 1). Sin iembargo, nunca ejecuta el conmutador fuera del rango 0..2.
Peter Cordes
@PeterCordes: tenga en cuenta que el código en la respuesta no lo hace do_general_stuffpor defecto, solo para el caso 2 (y 0 y 1).
Pete Becker
Una sugerencia: ¿quizás el caso predeterminado debería eliminarse o cambiarse de nombre? Es bastante fácil pasar por alto los diferentes nombres de funciones.
I.Am.A.Guy
@PeteBecker: Oh, IDK cómo me perdí eso generaly defaultfueron palabras diferentes. OTOH, es un hecho conocido que los humanos todavía pueden leer una palabra si las letras del medio están codificadas; Tendemos a mirar el principio y el final, por lo que tener solo el medio es diferente probablemente no sea ideal para la capacidad de deslizamiento. Quizás elimine el do_prefijo.
Peter Cordes
1
@supercat: Ese "hecho bien conocido" no se trata de reemplazar las letras del medio por otras diferentes, sino de mezclar el orden de entonces. "Si las cialm eran verdaderas en graneel, sería mejor que las raed".
Michael Karcher