Declaración de cambio: ¿debe ser el último caso predeterminado?


178

Considere la siguiente switchdeclaración:

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

Este código se compila, pero ¿es válido (= comportamiento definido) para C90 / C99? Nunca he visto código donde el caso predeterminado no sea el último.

EDITAR:
como Jon Cage escriben y KillianDS : este es un código realmente feo y confuso y lo sé muy bien. Solo estoy interesado en la sintaxis general (¿está definida?) Y el resultado esperado.


19
+1 Nunca consideró ese comportamiento
Jamie Wong

@ Péter Török: ¿quiere decir que si el valor == 2 devolverá 6?
Alexandre C.

44
@ Péter Török no, el orden no importa: si el valor coincide con la constante en cualquier caso, el control saltará a esa declaración después de la etiqueta; de lo contrario, el control saltará a la declaración que sigue a la etiqueta predeterminada si está presente.
Pete Kirkham

11
@Jon Cage gotono es malo. Los seguidores del culto de carga son! No se puede imaginar hasta qué extremo puede ir la gente para evitarlo gotoporque es tan malvado, lo que hace que su código sea un verdadero lío ilegible.
Patrick Schlüter

3
Utilizo gotoprincipalmente para simular algo así como una finallycláusula en funciones, donde los recursos (archivos, memoria) deben liberarse al detenerse, y repetir para cada caso de error una lista de freey closeno ayuda para la legibilidad. Sin embargo, hay un uso de gotoeso que me gustaría evitar pero que no puedo, es cuando quiero salir de un bucle y estoy dentro switchde ese bucle.
Patrick Schlüter

Respuestas:


83

El estándar C99 no es explícito sobre esto, pero tomando todos los hechos juntos, es perfectamente válido.

UNA case y defaultetiqueta son equivalentes a una gotoetiqueta. Ver 6.8.1 Declaraciones etiquetadas. Especialmente interesante es 6.8.1.4, que permite el dispositivo de Duff ya mencionado:

Cualquier declaración puede estar precedida por un prefijo que declare un identificador como nombre de etiqueta. Las etiquetas en sí mismas no alteran el flujo de control, que continúa sin obstáculos a través de ellas.

Editar : el código dentro de un interruptor no es nada especial; es un bloque de código normal como en una ifdeclaración, con etiquetas de salto adicionales. Esto explica el comportamiento de caída y por québreak es necesario.

6.8.4.2.7 incluso da un ejemplo:

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

En el fragmento de programa artificial, el objeto cuyo identificador es i existe con una duración de almacenamiento automática (dentro del bloque) pero nunca se inicializa y, por lo tanto, si la expresión de control tiene un valor distinto de cero, la llamada a la función printf accederá a un valor indeterminado. Del mismo modo, no se puede acceder a la llamada a la función f.

Las constantes de mayúsculas y minúsculas deben ser únicas dentro de una declaración de cambio:

6.8.4.2.3 La expresión de cada etiqueta de caso será una expresión constante entera y no dos de las expresiones de caso constante en la misma instrucción de cambio tendrán el mismo valor después de la conversión. Puede haber como máximo una etiqueta predeterminada en una declaración de cambio.

Todos los casos se evalúan, luego salta a la etiqueta predeterminada, si se proporciona:

6.8.4.2.5 Las promociones enteras se realizan en la expresión de control. La expresión constante en cada etiqueta de caso se convierte al tipo promovido de la expresión de control. Si un valor convertido coincide con el de la expresión de control promovida, el control salta a la declaración que sigue a la etiqueta de caso coincidente. De lo contrario, si hay una etiqueta predeterminada, el control salta a la declaración etiquetada. Si no coincide la expresión constante de caso convertido y no hay una etiqueta predeterminada, no se ejecuta ninguna parte del cuerpo del conmutador.


66
@HeathHunnicutt Claramente no entendiste el propósito del ejemplo. El código no está compuesto por este póster, sino tomado directamente del estándar C, como una ilustración de cuán extrañas son las declaraciones de cambio y qué tan mala práctica conducirá a errores. Si se hubiera molestado en leer el texto debajo del código, se daría cuenta de eso.
Lundin

2
+1 para compensar el voto negativo. Votar a alguien por citar el estándar C parece bastante duro.
Lundin

2
@Lundin No rechazo el estándar C, y no pasé por alto nada como usted sugiere. Voté en contra de la mala pedagogía de usar un ejemplo malo e innecesario. En particular, ese ejemplo se relaciona con una situación completamente diferente de la que se le preguntó. Podría continuar, pero "gracias por sus comentarios".
Heath Hunnicutt

12
Intel le dice que coloque el código más frecuente primero en una declaración de cambio en Branch and Loop Reorganization para evitar errores de predicción . Estoy aquí porque tengo un defaultcaso que domina otros casos en aproximadamente 100: 1, y no sé si es válido o no está definido para hacer defaultel primer caso.
JWW

@jww No estoy seguro de lo que quieres decir con Intel. Si te refieres a inteligencia, lo llamaré hipótesis. Pensé lo mismo, pero la lectura posterior afirma que, a diferencia de las declaraciones if, las declaraciones switch son de acceso aleatorio. Entonces, el último caso no es más lento de alcanzar que el primero. Esto se logra mediante el hash de los valores de caso constantes. Es por eso que las declaraciones de cambio son más rápidas que las declaraciones cuando las ramas son muchas.

91

Las declaraciones de caso y la declaración predeterminada pueden aparecer en cualquier orden en la declaración de cambio. La cláusula predeterminada es una cláusula opcional que coincide si ninguna de las constantes en las declaraciones de caso puede coincidir.

Buen ejemplo :-

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


Outputs '2,default'

muy útil si desea que sus casos se presenten en un orden lógico en el código (como en, sin decir caso 1, caso 3, caso 2 / predeterminado) y sus casos son muy largos, por lo que no desea repetir todo el caso código en la parte inferior para el valor predeterminado


77
Este es exactamente el escenario en el que generalmente coloco el valor predeterminado en otro lugar que no sea el final ... hay un orden lógico para los casos explícitos (1, 2, 3) y quiero que el valor predeterminado se comporte exactamente igual que uno de los casos explícitos que No es el último.
ArtOfWarfare

51

Es válido y muy útil en algunos casos.

Considere el siguiente 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
}

El punto es que el código anterior es más legible y eficiente que en cascada if. Podría poner defaultal final, pero no tiene sentido, ya que centrará su atención en los casos de error en lugar de los casos normales (que aquí es el defaultcaso).

En realidad, no es un buen ejemplo, pollya sabes cuántos eventos pueden ocurrir como máximo. Mi punto real es que no son casos con un conjunto definido de valores de entrada, donde hay 'excepciones' y los casos normales. Si es mejor poner excepciones o casos normales al frente es una cuestión de elección.

En el campo del software, pienso en otro caso muy habitual: recursiones con algunos valores terminales. Si puedes expresarlo usando un interruptor,default será el valor habitual que contiene la llamada recursiva y elementos distinguidos (casos individuales) los valores del terminal. Por lo general, no es necesario centrarse en los valores terminales.

Otra razón es que el orden de los casos puede cambiar el comportamiento del código compilado, y eso es importante para las actuaciones. La mayoría de los compiladores generarán código de ensamblaje compilado en el mismo orden en que aparece el código en el conmutador. Eso hace que el primer caso sea muy diferente de los demás: todos los casos, excepto el primero, implicarán un salto y eso vaciará las tuberías del procesador. Puede entenderlo como un predictor de rama que por defecto ejecuta el primer caso que aparece en el conmutador. Si un caso es mucho más común que los otros, entonces tiene muy buenas razones para ponerlo como el primer caso.

La lectura de comentarios es la razón específica por la cual el póster original hizo esa pregunta después de leer la reorganización del compilador de Intel Branch Loop sobre la optimización del código.

Entonces se convertirá en un arbitraje entre la legibilidad del código y el rendimiento del código. Probablemente sea mejor hacer un comentario para explicar al futuro lector por qué aparece primero un caso.


66
+1 por dar un (buen) ejemplo sin el comportamiento fallido.
KillianDS

1
... pensando en ello, no estoy convencido de que el valor predeterminado en la parte superior sea bueno porque muy pocas personas lo buscarían allí. Podría ser mejor asignar el retorno a una variable y manejar el éxito en un lado de un if y los errores en el otro lado con una declaración de caso.
Jon Cage

@ Jon: solo escríbelo. Agrega ruido sintáctico sin ningún beneficio de legibilidad. Y, si el valor predeterminado está en la parte superior, realmente no hay necesidad de mirarlo, es realmente obvio (podría ser más complicado si lo coloca en el medio).
kriss

Por cierto, no me gusta mucho la sintaxis C switch / case. Preferiría poder poner varias etiquetas después de un caso en lugar de estar obligado a poner varias sucesivas case. Lo que es deprimente es que se ve igual que el azúcar sintáctico y no romperá ningún código existente si es compatible.
kriss

1
@kriss: Estaba medio tentado a decir "¡Yo tampoco soy un programador de Python!" :)
Andrew Grimm

16

Sí, esto es válido y, en algunas circunstancias, incluso es útil. En general, si no lo necesita, no lo haga.


-1: Esto huele a maldad para mí. Sería mejor dividir el código en un par de declaraciones de cambio.
Jon Cage

25
@John Cage: ponerme un -1 aquí es desagradable. No es mi culpa que este sea un código válido.
Jens Gustedt

Por curiosidad, me gustaría saber en qué circunstancias es útil.
Salil

1
El -1 fue dirigido a su afirmación de que es útil. Lo cambiaré a +1 si puede proporcionar un ejemplo válido para respaldar su reclamo.
Jon Cage

44
A veces, al cambiar por un error que obtuvimos a cambio de alguna función del sistema. Digamos que tenemos un caso en el que sabemos para bien que tenemos que hacer una salida limpia, pero esta salida limpia puede requerir algunas líneas de codificación que no queremos repetir. Pero supongamos que también tenemos muchos otros códigos de error exóticos que no queremos manejar individualmente. Consideraría simplemente poner un perror en el caso predeterminado y dejar que se ejecute en el otro caso y salir limpiamente. No digo que debas hacerlo así. Es solo cuestión de gustos.
Jens Gustedt

8

No hay un orden definido en una declaración de cambio. Puede ver los casos como algo así como una etiqueta con nombre, como una gotoetiqueta. Contrariamente a lo que la gente parece pensar aquí, en el caso del valor 2 no se salta a la etiqueta predeterminada. Para ilustrar con un ejemplo clásico, aquí está el dispositivo de Duff , que es el cartel hijo de los 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);
  }
}

44
Y para cualquiera que no esté familiarizado con el dispositivo de Duff, este código es completamente ilegible ...
KillianDS

7

Un escenario en el que consideraría apropiado tener un 'valor predeterminado' ubicado en otro lugar que no sea el final de una declaración de caso es en una máquina de estado donde un estado no válido debería restablecer la máquina y proceder como si fuera el estado inicial. Por ejemplo:

interruptor (widget_state)
{
  predeterminado: / * Se cayó de los rieles - reiniciar y continuar * /
    widget_state = WIDGET_START;
    /* Caer a través */
  caso WIDGET_START:
    ...
    descanso;
  caso WIDGET_WHATEVER:
    ...
    descanso;
}

Una disposición alternativa, si un estado no válido no debe restablecer la máquina, pero debe ser fácilmente identificable como un estado no válido:

interruptor (widget_state) { caso WIDGET_IDLE: widget_ready = 0; widget_hardware_off (); descanso; caso WIDGET_START: ... descanso; caso WIDGET_WHATEVER: ... descanso; defecto: widget_state = WIDGET_INVALID_STATE; /* Caer a través */ caso WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off (); ... haga lo que sea necesario para establecer una condición "segura" }

El código en otro lugar puede buscar (widget_state == WIDGET_INVALID_STATE) y proporcionar cualquier comportamiento de informe de error o restablecimiento de estado que parezca apropiado. Por ejemplo, el código de barra de estado podría mostrar un icono de error, y la opción de menú "widget de inicio" que está deshabilitada en la mayoría de los estados no inactivos podría habilitarse para WIDGET_INVALID_STATE así como WIDGET_IDLE.


6

Intercambiando con otro ejemplo: Esto puede ser útil si "predeterminado" es un caso inesperado, y desea registrar el error pero también hacer algo sensato. Ejemplo de algunos de mi propio código:

  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

Hay casos en los que está convirtiendo ENUM en una cadena o convirtiendo cadena en enum en caso de que esté escribiendo / leyendo a / desde un archivo.

En ocasiones, debe establecer uno de los valores predeterminados para cubrir los errores cometidos al editar archivos manualmente.

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

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

2

La defaultcondición puede ser en cualquier lugar dentro del conmutador que puede existir una cláusula de caso. No es obligatorio ser la última cláusula. He visto código que pone el valor predeterminado como la primera cláusula. loscase 2: se ejecuta normalmente, aunque la cláusula predeterminada está por encima de él.

Como prueba, puse el código de muestra en una función, llamé test(int value){}y ejecuté:

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

El resultado es:

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

1

Es válido, pero bastante desagradable. Sugeriría que en general es malo permitir fallos, ya que puede conducir a un código de espagueti muy desordenado.

Es casi seguro que sea mejor dividir estos casos en varias declaraciones de cambio o funciones más pequeñas.

[edit] @Tristopia: Tu ejemplo:

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

sería más claro en cuanto a su intención (creo) si se escribiera así:

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: Su segundo ejemplo es probablemente el ejemplo más claro de un buen uso para el seguimiento:

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

... pero personalmente dividiría el reconocimiento de comentarios en su propia función:

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
Hay casos en los que caer es realmente una muy buena idea.
Patrick Schlüter

Ejemplo de conversión de UCS-2 a UTF-8 res la matriz de destino, wces el wchar_t conmutador de entrada (utf8_length) {/ * Nota: ¡el código cae por los casos! * / caso 3: r [2] = 0x80 | (wc y 0x3f); wc >> = 6; wc | = 0x800; caso 2: r [1] = 0x80 | (wc y 0x3f); wc >> = 6; wc | = 0xc0; caso 1: r [0] = wc; }
Patrick Schlüter

Aquí otro, una rutina de copia de cadena con escape de caracteres: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter

Sí, pero esta rutina es uno de nuestros puntos calientes, esta fue la forma más rápida y portátil de implementarla (no haremos el montaje). Tiene solo 1 prueba para cualquier longitud UTF, la suya tiene 2 o incluso 3. Además, no se me ocurrió, lo tomé de BSD.
Patrick Schlüter

1
Sí, especialmente en conversiones en búlgaro y griego (en Solaris SPARC) y texto con nuestro marcado interno (que es de 3 bytes UTF8). Admitido, en total no es mucho y se ha vuelto irrelevante desde nuestra última actualización de hardware, pero en el momento en que se escribió, marcó una diferencia.
Patrick Schlüter
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.