Capítulo 4: Enumeraciones y Switch Statements
Las enumeraciones te permiten crear tus propios tipos con un conjunto fijo de
valores nombrados. En lugar de usar números "mágicos" como 0, 1, 2, usas nombres
significativos como Status::Connected. Esto hace tu código más legible
y seguro.
Objetivos
- Entender qué es y para qué sirve
enum class - Crear y usar tus propias enumeraciones
- Dominar el
switchstatement para control de flujo - Conocer las diferencias entre
enumyenum class - Evitar el bug común de "fall-through"
El problema: números mágicos
Imagina que estás programando una conexión de red. Podrías representar el estado usando números enteros para cada condición:
// [X] Código confuso con "números mágicos"
int connection_status = 2;
if (connection_status == 0) {
std::cout << "Desconocido
";
} else if (connection_status == 1) {
std::cout << "Conectando
";
} else if (connection_status == 2) {
std::cout << "Conectado
";
}
// Que significa 2? Por que no 3? Confuso... ¿Qué son "números mágicos"? Son valores literales (como 0, 1, 2) cuyo
significado no es evidente al leerlos. Se llaman "mágicos" porque parecen aparecer de la
nada sin explicación. Cuando lees if (status == 2), no tienes idea de qué
significa "2" sin consultar documentación o comentarios.
¿Por qué son problemáticos? Los números mágicos hacen el código difícil de mantener. Si necesitas agregar un nuevo estado, debes encontrar todos los lugares donde se usa ese número. Si dos desarrolladores asignan el mismo número a diferentes estados en partes diferentes del código, tendrás bugs sutiles y difíciles de rastrear.
- No es obvio qué significa cada número
- Puedes asignar valores inválidos:
connection_status = 999; - Dificulta el mantenimiento y lectura del código
- Propenso a errores de lógica
La solución: enum class
#include <iostream>
// Definición de enumeración
enum class Status { Unknown, Connecting, Connected, Disconnected };
int main() {
Status connection_status = Status::Connected;
if (connection_status == Status::Connected) {
std::cout << "Estamos conectados!
";
}
return 0;
} Ventajas
- ✅ Legible:
Status::Connectedes autoexplicativo - ✅ Type-safe: No puedes asignar valores inválidos
- ✅ Scope: Los valores están encapsulados en el enum
- ✅ Mantenible: Fácil agregar nuevos estados
Anatomía de enum class
Estructura de una Enumeración
flowchart TB
A["enum class Status"] --> B["enum class"]
A --> C["Status"]
A --> D["Valores"]
B --> B1["Palabra clave<br/>enum class"]
C --> C1["Nombre del tipo"]
D --> D1["Valores enumerados<br/>(separados por comas)"]
E["Uso:"] --> E1["Status::Unknown"]
E1 --> E2["Status = Tipo"]
E1 --> E3[":: = Operador de scope"]
E1 --> E4["Unknown = Valor"]
style A fill:#e1f5fe,stroke:#0277bd,stroke-width:3px
style B fill:#fff3e0,stroke:#e65100,stroke-width:2px
style C fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
style D fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px Por defecto, cada valor de un enum internamente se asigna automáticamente un número entero comenzando desde 0.
Por defecto, un enum class usa tipo subyacente de enum como almacenamiento (típicamente 4 bytes), pero puedes especificar otro tipo más pequeño.
enum vs enum class: Cual es la diferencia? Intermedio
C++ tiene dos formas de definir enumeraciones: enum (estilo C) y
enum class (C++11, recomendado).
// Estilo viejo: enum (sin class)
enum OldStatus { Unknown, Connected };
// Uso:
OldStatus s = Connected; // No requiere OldStatus::
int x = Connected; // [X] Se convierte implícitamente a int!
// Estilo moderno: enum class
enum class NewStatus { Unknown, Connected };
// Uso:
NewStatus s = NewStatus::Connected; // [OK] Requiere scope
int x = NewStatus::Connected; // [X] ERROR: no convierte a int Siempre usa enum class en código nuevo. Es más
seguro porque:
- Los valores no contaminan el scope global
- No se convierten implícitamente a int (previene bugs)
- Mejor soporte para type checking del compilador
Switch statements
El switch es perfecto para trabajar con enums. Evalúa una expresión
y ejecuta código diferente según el caso:
enum class Status { Unknown, Connecting, Connected, Disconnected };
void print_status(Status s) {
switch (s) {
case Status::Unknown:
std::cout << "Estado desconocido
";
break;
case Status::Connecting:
std::cout << "Conectando...
";
break;
case Status::Connected:
std::cout << "¡Conectado!
";
break;
case Status::Disconnected:
std::cout << "Desconectado
";
break;
default:
std::cout << "Estado inválido
";
break;
}
} Componentes del switch
switch (expresión)- Evalúa la expresión una vezcase valor:- Si la expresión == valor, ejecuta este bloquebreak;- Sale del switch (MUY IMPORTANTE)default:- Se ejecuta si ningún case coincide (opcional)
Si olvidas el break, la ejecución "cae" al siguiente caso:
switch (s) {
case Status::Connecting:
std::cout << "Conectando...
";
// [X] ¡FALTA break!
case Status::Connected:
std::cout << "Conectado
";
break;
}
// Si s == Status::Connecting, imprime:
// Conectando...
// Conectado ← ¡También ejecutó este caso!
Esto es casi siempre un bug. Algunos compiladores te advierten con -Wimplicit-fallthrough.
A veces quieres fall-through intencional para que múltiples casos ejecuten el mismo código:
enum class Day { Mon, Tue, Wed, Thu, Fri, Sat, Sun };
void check_day(Day d) {
switch (d) {
case Day::Sat:
case Day::Sun:
std::cout << "¡Fin de semana!
";
break;
case Day::Mon:
case Day::Tue:
case Day::Wed:
case Day::Thu:
case Day::Fri:
std::cout << "Día laboral
";
break;
}
} Como implementa el compilador un switch? Avanzado
El compilador optimiza el switch según el número y distribución de casos.
Para casos consecutivos (0, 1, 2, 3...), suele generar una jump table
(tabla de saltos): un array de direcciones donde cada índice corresponde a un caso. Esto
hace que el switch sea O(1) - tiempo constante, sin importar cuántos casos tengas.
Para casos dispersos (0, 100, 1000...), el compilador puede usar binary search (búsqueda binaria) o simplemente una cadena de if-else. La elección depende de heurísticas del compilador sobre qué será más eficiente según el número de casos y su distribución.
Puedes ver qué estrategia usa tu compilador generando el código assembly:
g++ -S -O2 file.cc
Ejemplo completo
#include <iostream>
enum class UserPermission { Read = 1, Write = 2, Execute = 4, Admin = 8 };
enum class Status { Unknown, Connecting, Connected, Disconnected };
void print_permission(UserPermission p) {
switch (p) {
case UserPermission::Read:
std::cout << "Permiso de lectura
";
break;
case UserPermission::Write:
std::cout << "Permiso de escritura
";
break;
case UserPermission::Execute:
std::cout << "Permiso de ejecución
";
break;
case UserPermission::Admin:
std::cout << "Permiso de administrador
";
break;
default:
std::cout << "Permiso desconocido
";
break;
}
}
int main() {
Status s = Status::Connected;
switch (s) {
case Status::Unknown:
std::cout << "?
";
break;
case Status::Connecting:
std::cout << "...
";
break;
case Status::Connected:
std::cout << "✓
";
break;
case Status::Disconnected:
std::cout << "✗
";
break;
}
UserPermission perm = UserPermission::Admin;
print_permission(perm);
// Convertir a int si realmente lo necesitas
int perm_value = static_cast<int>(perm);
std::cout << "Valor: " << perm_value << "
"; // 8
return 0;
} Checklist
- Sé declarar un
enum classcon valores nombrados - Entiendo que los valores por defecto empiezan en 0
- Uso la sintaxis
NombreEnum::Valorcorrectamente - Puedo escribir un
switchcompleto con todos los casos - NUNCA olvido el
breakal final de cada caso - Sé cuándo usar
default:en un switch - Prefiero
enum classsobreenumsimple
Preguntas para reflexionar
¿Qué pasa si omito el break en un caso del switch?
Se produce fall-through: la ejecución continúa al siguiente caso sin detenerse.
switch (x) {
case 1: cout << 'A'; // sin break!
case 2: cout << 'B'; break;
}
// Si x=1, imprime: ABEsto es casi siempre un bug. Algunos compiladores te advierten con -Wimplicit-fallthrough.
¿Puedo usar switch con strings? ¿Por qué sí o por qué no?
No. El switch solo funciona con tipos integrales (int, char, enum).
Las strings son objetos (arrays de caracteres), no valores primitivos. Para comparar strings usa if-else:
if (s == 'hola') { ... }
else if (s == 'adiós') { ... }En C++20+ puedes simular switch con strings usando un mapa de funciones o pattern matching.
¿Cómo convierto un enum class a su valor entero?
Usa static_cast:
enum class Status { Unknown = 0, Connected = 1 };
Status s = Status::Connected;
int value = static_cast<int>(s); // 1El cast es explícito (no automático) para evitar conversiones accidentales y mejorar la seguridad de tipos.
¿Es obligatorio incluir default: en todo switch?
No es obligatorio, pero es muy recomendado.
- Si cubres todos los casos del enum, el compilador puede advertirte si falta alguno (con
-Wswitch) - Si no cubres todos, el
default:maneja casos inesperados
switch (status) {
case A: ...; break;
case B: ...; break;
default: cerr << 'Error!'; break;
} ¿Por qué enum class es más seguro que enum simple?
3 razones clave:
- Scope: Los valores no contaminan el namespace global (
Status::ConnectedvsConnected) - No conversión implícita: No se convierten automáticamente a
int, previniendo bugs - Type safety: No puedes mezclar enums de diferentes tipos
enum Old { A };
int x = A; // OK (peligroso!)
enum class New { A };
int y = New::A; // ERROR ✓ Usa switch cuando:
- Comparas una variable contra múltiples valores constantes
- Trabajas con enums
- Tienes 3 o más casos
Usa if-else cuando:
- Tienes condiciones complejas (
x > 10 && y < 20) - Solo tienes 1-2 casos
- Comparas con variables (no constantes)
En el Capítulo 4 aprenderás sobre structs (estructuras), que te permiten agrupar múltiples variables relacionadas en un solo tipo. Es el primer paso hacia la programación orientada a objetos.