switch a fondo
La semántica real de switch como salto a etiqueta: el fallthrough y el atributo [[fallthrough]] de C23, las estrategias de codegen del compilador (tabla de salto, búsqueda binaria, bitmask) y el patrón de máquina de estados.
switch no es una cadena de if disfrazada. Es una construcción con semántica propia: evalúa una expresión entera y salta a una etiqueta situada dentro de un bloque. De ahí salen todas sus rarezas —el fallthrough, el caso de Duff, los case incrustados dentro de un bucle— y también su superpoder: es la única forma en C de que el compilador convierta una decisión de N ramas en un único salto indirecto de coste constante.
- Entender
switchcomo salto a etiqueta, no como comparación en cadena. - Dominar el fallthrough y documentarlo con
[[fallthrough]]de C23. - Reconocer las estrategias de codegen: tabla de salto, búsqueda binaria y bitmask.
- Construir máquinas de estados legibles y rápidas con
switch.
Qué es realmente un switch
El estándar es sorprendentemente escueto: el cuerpo de un switch es una sentencia —casi siempre un bloque— y case y default son simplemente etiquetas, hermanas de las que usa goto. La construcción evalúa la expresión de control, le aplica las promociones enteras, busca la etiqueta cuyo valor coincida y salta ahí. Después, la ejecución continúa linealmente hasta encontrar un break o el final del bloque.
Esa lectura explica el famoso caso de Duff, legal precisamente porque case es una etiqueta y nada impide que viva dentro de otra sentencia:
/* El dispositivo de Duff: desenrollado manual con entrada a mitad de bucle.
Requiere n mayor que cero. */
void enviar(volatile char *puerto, const char *src, size_t n)
{
size_t bloques = (n + 7) / 8;
switch (n % 8) {
case 0: do { *puerto = *src++;
case 7: *puerto = *src++;
case 6: *puerto = *src++;
case 5: *puerto = *src++;
case 4: *puerto = *src++;
case 3: *puerto = *src++;
case 2: *puerto = *src++;
case 1: *puerto = *src++;
} while (--bloques > 0);
}
}
Las reglas duras que conviene tener grabadas: los valores de case deben ser expresiones constantes enteras, no puede haber dos iguales, solo cabe un default, y la expresión de control ha de ser de tipo entero (nada de float, nada de cadenas). Y un detalle que muerde: si declaras una variable con inicializador justo tras la llave de apertura, el salto la esquiva y su valor queda indeterminado.
Escribir una declaración inicializada como primera línea del cuerpo del switch compila, pero el salto a un case pasa por encima del inicializador. La variable existe, su valor no. Si necesitas locales por rama, abre un bloque propio dentro de cada case.
Cómo lo compila el compilador
Aquí está lo que separa switch de una cadena de if. El compilador analiza el conjunto de valores y elige una estrategia de generación de código. No hay una única traducción: hay una decisión de coste.
Tabla de salto
Valores densos y contiguos. El compilador emite un array de direcciones y un salto indirecto. Coste constante, independiente del número de casos.
Búsqueda binaria
Valores dispersos pero numerosos. Genera un árbol de comparaciones con coste logarítmico en el número de casos, sin memoria extra.
Bitmask o test de rango
Muchos casos que hacen lo mismo dentro de un rango estrecho. Se comprime en una máscara y un par de instrucciones de test.
Cadena de comparaciones
Dos o tres casos. Montar una tabla saldría más caro que comparar; el compilador degrada a if encadenados.
flowchart TD A[switch con N casos] --> B[Los valores son densos y contiguos] B -->|si| C[Tabla de salto: un salto indirecto de coste constante] B -->|no| D[Cuantos casos hay] D -->|dos o tres| E[Cadena de comparaciones] D -->|muchos y dispersos| F[Arbol de busqueda binaria] C --> Z[El compilador decide solo: tu solo describes la intencion] E --> Z F --> Z style C fill:#a6e3a1,color:#11111b style F fill:#89b4fa,color:#11111b
La consecuencia práctica es fuerte: una cadena de if sobre el mismo entero no puede transformarse con la misma libertad, porque cada condición es una expresión arbitraria que podría tener efectos. Al escribir switch firmas un contrato —la expresión se evalúa una vez, los valores son constantes, no hay solapes— y ese contrato es lo que habilita la tabla de salto.
Fallthrough y el atributo de C23
El fallthrough es la consecuencia inevitable de la semántica de etiqueta: si no hay break, la ejecución sigue hacia abajo. Es útil unas pocas veces al año y es un bug las otras. C23 zanja la ambigüedad con [[fallthrough]]:
enum resultado clasificar(int c)
{
switch (c) {
case ' ':
case '\t': /* agrupar etiquetas no es fallthrough */
return ESPACIO;
case '\r':
consumir_cr();
[[fallthrough]]; /* C23: el salto de aquí es intencional */
case '\n':
nueva_linea();
break;
default:
return OTRO;
}
return FIN_LINEA;
}
Dos matices que se confunden. Primero: apilar case sin código entre ellos no es fallthrough y no necesita el atributo; el aviso solo salta cuando hay sentencias que se atraviesan. Segundo: [[fallthrough]]; es una sentencia con atributo, lleva punto y coma y debe ser lo último antes de la siguiente etiqueta. Compila siempre con -Wimplicit-fallthrough para que el compilador cace los accidentales mientras respeta los que marcas.
El patrón de máquina de estados
La aplicación reina de switch es la máquina de estados: un enum de estados, un bucle y un switch que despacha. El compilador convierte el despacho en una tabla de salto y el resultado compite con implementaciones a mano.
enum estado { INICIO, EN_PALABRA, EN_COMENTARIO, FIN };
size_t contar_palabras(const char *s)
{
enum estado e = INICIO;
size_t n = 0;
for (; e != FIN; s++) {
switch (e) {
case INICIO:
if (*s == '\0') e = FIN;
else if (*s == '#') e = EN_COMENTARIO;
else if (*s != ' ') { n++; e = EN_PALABRA; }
break;
case EN_PALABRA:
if (*s == '\0') e = FIN;
else if (*s == ' ') e = INICIO;
break;
case EN_COMENTARIO:
if (*s == '\0') e = FIN;
else if (*s == '\n') e = INICIO;
break;
case FIN:
break;
}
}
return n;
}
Truco de diseño: usa un enum como expresión de control y omite el default. Con -Wswitch, el compilador te avisará cada vez que añadas un estado nuevo y olvides tratarlo. Un default que solo hace break desactiva esa red de seguridad; consérvala mientras el conjunto de estados sea cerrado.
La diferencia entre switch y una cadena de if no está en la sintaxis: está en cuánta información le entregas al compilador. Al escribir switch afirmas que hay una expresión, evaluada una vez, contrastada contra un conjunto cerrado, constante y sin duplicados de valores. Esa afirmación es lo que le permite razonar globalmente sobre las N ramas a la vez y elegir entre tabla de salto, árbol binario o máscara de bits. Con if encadenados le entregas N condiciones opacas que debe evaluar en orden porque cualquiera podría tener efectos. Es el mismo principio que atraviesa toda la ingeniería de C moderno —const, restrict, [[fallthrough]], [[nodiscard]]—: cada garantía que expresas explícitamente es una restricción que el optimizador puede explotar. El código rápido en C rara vez nace de trucos; nace de decirle a la máquina, con precisión, lo que no puede pasar.
- Escribe un
switchcon casos 0 a 7 y compílalo con-O2 -S; localiza la tabla de salto en el ensamblador. - Cambia los valores a 1, 100, 5000 y 90000 y compara: verás comparaciones o un árbol, no una tabla.
- Provoca un fallthrough accidental y arréglalo con
[[fallthrough]]bajo-Wimplicit-fallthrough. - Implementa la máquina de estados de arriba sin
defaulty añade un estado nuevo: comprueba que-Wswitchte delata. - Reescribe el caso de Duff con un bucle normal y mide si el compilador iguala su rendimiento con
-O3.