Para asegurar que tu código haga lo que debe, necesitas mecanismos de verificación que detecten fallos en distintos momentos. Existen tres niveles principales: la verificación en tiempo de compilación, la verificación en tiempo de ejecución (runtime) y las pruebas de lógica aplicadas por el programador.
El primer nivel es el static_assert [C++11], que actúa sobre la estructura misma del código. No se ejecuta cuando el programa corre, sino que el compilador lo evalúa mientras construye el ejecutable. Si la condición no se cumple, el compilador detiene el proceso y lanza un error, impidiendo que se genere un binario potencialmente corrupto. Esto es ideal para validar que los tipos de datos tengan el tamaño esperado o que una plantilla (template) reciba el tipo correcto.
El segundo nivel es el assert, que reside en la cabecera <cassert>. Este mecanismo verifica una condición mientras el programa se está ejecutando. Si la condición es falsa, el programa invoca a std::abort(), deteniéndose inmediatamente y mostrando el archivo y la línea donde ocurrió el desastre. Lo crucial aquí es que assert está diseñado para detectar invariantes: premisas lógicas que, si fallan, significan que el programador cometió un error de lógica (un bug), no que el usuario haya hecho algo mal. Por eficiencia, cuando compilas para producción (usando la macro NDEBUG), el compilador elimina todos los assert del código, de modo que no penalizan el rendimiento del producto final.
Finalmente, para verificar la lógica de negocio sin depender de frameworks externos como Google Test, puedes implementar tus propias funciones de test que devuelvan un bool o lancen excepciones, y coordinarlas en el main.
¿Cuándo usar cada uno? Usa static_assert para reglas de tipos y arquitectura; usa assert para errores de lógica interna (cosas que “nunca deberían pasar”); y usa pruebas manuales para validar que tus funciones devuelven los resultados correctos. Si confías en assert para manejar errores externos (como un archivo que no existe), romperás tu programa: en modo desarrollo el programa explotará de forma controlada, pero en producción, al estar deshabilitado, el programa ignorará el error y fallará silenciosamente más adelante, creando un bug imposible de rastrear.
#include <iostream>
#include <cassert>
#include <string>
#include <stdexcept>
// Una función "pura": no tiene efectos secundarios y siempre devuelve
// el mismo resultado para la misma entrada. Es el estándar de oro para testear.
int calcular_descuento(int precio, int porcentaje) {
// Invariante: el descuento no puede ser negativo ni mayor al 100%
// Si esto falla, hay un error de lógica en la función que la llama.
assert(porcentaje >= 0 && porcentaje <= 100);
return precio - (precio * porcentaje / 100);
}
// Verificación en tiempo de compilación [C++11].
// Si el tamaño de int no es el esperado, el código ni siquiera compila.
static_assert(sizeof(int) >= 2, "Este sistema requiere tipos enteros de al menos 16 bits.");
// Una función de test manual para nuestra lógica.
// En un entorno profesional, esto sería reemplazado por un framework.
bool test_calcular_descuento() {
if (calcular_descuento(100, 20) != 80) return false;
if (calcular_descuento(50, 0) != 50) return false;
if (calcular_descuento(200, 100) != 0) return false;
return true;
}
// Función que simula un error externo (ej. falta de conexión a base de datos).
// Aquí NO usamos assert, usamos excepciones porque es un error de runtime esperado.
void conectar_a_servidor(bool conectado) {
if (!conectado) {
throw std::runtime_error("Error: No se pudo conectar al servidor.");
}
}
int main() {
// 1. Ejecución de pruebas manuales antes de iniciar la lógica principal
if (test_calcular_descuento()) {
std::cout << "[PASS] test_calcular_descuento\n";
} else {
std::cerr << "[FAIL] test_calcular_descuento\n";
return 1; // Salimos con error
}
// 2. Uso de assert en runtime
int precio_usuario = 150;
int descuento_usuario = 10;
// Si el usuario introdujera un -5, el assert detendría el programa en desarrollo.
calcular_descuento(precio_usuario, descuento_usuario);
std::cout << "[INFO] Descuento aplicado con éxito.\n";
// 3. Gestión de errores reales (Runtime Error Handling)
try {
bool hay_internet = false;
conectar_a_servidor(hay_internet);
} catch (const std::exception& e) {
std::cerr << "[ERROR EXTERNO] " << e.what() << "\n";
}
return 0;
}
Análisis del ejemplo
El código comienza con un static_assert. Si intentaras compilar este código en una arquitectura extremadamente limitada donde int midiera solo 1 byte, el compilador lanzaría un error de texto descriptivo y el proceso se detendría, protegiendo tu integridad de tipos antes de que el programa existiera.
En la función calcular_descuento, aplicamos un assert para validar una invariante. No estamos comprobando si el usuario es un tonto por poner un descuento de 110%; estamos asegurando que nuestra lógica interna es matemáticamente coherente. Si la condición porcentaje >= 0 && porcentaje <= 100 es falsa, el programa se detiene inmediatamente con un error de ejecución.
La función test_calcular_descuento implementa una estrategia de testing manual. Comprueba varios escenarios (valor normal, límite inferior, límite superior) y devuelve un bool. En el main, verificamos este resultado; si falla, abortamos con un código de salida distinto de cero, lo cual es la forma básica en la que los sistemas de integración continua (CI) detectan fallos.
Finalmente, observa la diferencia crítica entre el assert y el std::runtime_error. En conectar_a_servidor, usamos una excepción. Esto es porque que no haya internet es un error del “mundo real”, no un error de nuestra lógica de programación. Las excepciones siempre están presentes, mientras que el assert desaparecerá en la versión de producción para que el programa sea lo más rápido posible.
El error frecuente
Un error clásico de principiante es usar assert para validar entradas de usuario o estados externos.
// ERROR GRAVE
void abrir_archivo(std::string nombre) {
// ERROR: El usuario puede escribir un nombre de archivo inválido.
// Esto NO es un error de lógica del programador, es un error de entorno.
assert(!nombre.empty());
// Si compilas en modo Release, el assert se borra.
// Si el nombre está vacío, el programa continuará y fallará más abajo
// de forma errática o crashearás en un punto muy lejano.
std::cout << "Abriendo: " << nombre << "\n";
}
Si compilas con la bandera -DNDEBUG (estándar para builds de Release), el compilador simplemente ignora la línea del assert. Si el archivo está vacío, el programa intentará procesarlo sin protección, causando un comportamiento indefinido o un crash difícil de localizar. Para situaciones externas, usa siempre excepciones o tipos como std::optional.
N° 122