Cuando ejecutas throw expr, el runtime de C++ no simplemente salta a una dirección de memoria; crea un objeto de la excepción y lo almacena en un área de memoria especial, fuera de la pila local, garantizando que el objeto sobreviva mientras se propaga por la pila de llamadas. Este mecanismo de despegado de errores permite separar la lógica de negocio del manejo de fallos, pero debe usarse solo para situaciones verdaderamente excepcionales y no para controlar el flujo normal del programa. Si no interceptas la excepción mediante un bloque try-catch, el runtime llamará a std::terminate, abortando el proceso.
Para evitar el slicing (recorte de objetos), es imperativo capturar las excepciones por referencia (const T&). Si capturas por valor, si se lanza un objeto derivado, se creará una copia de la clase base, perdiendo la información de la clase derivada y el comportamiento polimórfico del método what(). La jerarquía estándar de std::exception [C++98] se divide en std::logic_error (para errores que el programador podría haber evitado mediante una lógica correcta, como std::invalid_argument o std::out_of_range) y std::runtime_error (para errores que dependen de factores externos o del estado del sistema, como std::overflow_error). Existen también excepciones de sistema como std::bad_alloc cuando new falla, o std::bad_cast cuando un dynamic_cast de una referencia falla.
Si decides capturar una excepción pero no puedes resolverla en ese nivel, puedes usar throw; para relanzarla. Esto es crucial porque preserva el tipo original de la excepción mientras la propaga hacia arriba. Un detalle crítico ocurre en los constructores: si un constructor lanza una excepción, el objeto se considera “no construido” y su propio destructor no se ejecutará; sin embargo, todos los miembros que se hayan construido correctamente antes del error sí verán sus destructores llamados, garantizando la integridad de los recursos mediante RAII.
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
// Excepción personalizada basada en runtime_error
class DataCorruptedException : public std::runtime_error {
public:
explicit DataCorruptedException(const std::string& msg)
: std::runtime_error(msg) {}
};
class DatabaseConnection {
public:
DatabaseConnection() {
// Si el constructor lanza una excepción, el objeto no existe formalmente
// y su destructor no se llamará, pero sus miembros sí lo harán.
throw std::runtime_error("Error crítico: No se pudo conectar a la base de datos");
}
~DatabaseConnection() {
std::cout << "Destructor DatabaseConnection ejecutado\n";
}
};
void validate_input(int value) {
if (value < 0) {
// Error de lógica: el programador debería haber validado el dato antes
throw std::invalid_argument("El valor no puede ser negativo");
}
if (value > 1000) {
// Error de runtime: el dato externo es inconsistente
throw DataCorruptedException("Datos corruptos detectados en el flujo");
}
}
void process_data(int value) {
try {
validate_input(value);
std::cout << "Procesando valor: " << value << "\n";
} catch (const std::invalid_argument& e) {
std::cerr << "Error de validación (Lógica): " << e.what() << "\n";
throw; // Relanzamos la excepción para que el llamador la gestione
} catch (const DataCorruptedException& e) {
std::cerr << "Error de datos (Runtime): " << e.what() << "\n";
// Aquí decidimos no relanzar; el error se gestiona localmente
} catch (const std::exception& e) {
std::cerr << "Error genérico inesperado: " << e.what() << "\n";
throw;
}
}
int main() {
// Caso 1: Excepción en constructor
try {
DatabaseConnection db;
} catch (const std::exception& e) {
std::cerr << "Captura en main (Constructor): " << e.what() << "\n";
}
// Caso 2: Error de lógica y re-throw
std::cout << "\n--- Iniciando Caso 2 ---\n";
try {
process_data(-5);
} catch (const std::invalid_argument& e) {
std::cerr << "main capturó el error de lógica: " << e.what() << "\n";
}
// Caso 3: Error de runtime personalizado
std::cout << "\n--- Iniciando Caso 3 ---\n";
try {
process_data(5000);
} catch (const std::exception& e) {
std::cerr << "main capturó error genérico: " << e.what() << "\n";
}
// Caso 4: Catch-all (atrapa cualquier cosa, incluso tipos no derivados de std::exception)
std::cout << "\n--- Iniciando Caso 4 ---\n";
try {
throw 404; // Lanzar un entero no es buena práctica, pero es legal
} catch (...) {
std::cerr << "Captura total (catch-all) de un tipo desconocido\n";
}
return 0;
}
Análisis del ejemplo
En el código anterior, el objeto DatabaseConnection lanza una std::runtime_error en su constructor. Como el objeto nunca llega a estar completamente construido, su destructor no se ejecuta, evitando fugas de recursos si otros miembros hubieran fallado antes.
En la función process_data, aplicamos el principio de capturar por referencia constante (const std::invalid_argument& e). Esto permite que, gracias al polimorfismo, si lanzáramos una clase derivada de std::invalid_argument, el método what() nos devuelva el mensaje específico de la clase derivada. Si hubiéramos usado catch (std::invalid_argument e) (por valor), sufriríamos slicing y solo veríamos la información de la clase base.
Observa el uso de throw; en el bloque catch (const std::invalid_argument& e). Esto le dice al compilador: “he registrado el error, pero no puedo arreglarlo; pásalo al siguiente nivel”. Es vital usar throw; sin argumentos; usar throw e; sería un error grave que lanzaría una copia de la excepción, perdiendo el polimorfismo y el tipo original.
Finalmente, el bloque catch (...) en el main actúa como una red de seguridad última. Es útil en la frontera entre código C++ y C, o en la función main para evitar un cierre abrupto del programa, aunque en lógica de negocio compleja, silenciar excepciones con ... suele ocultar errores críticos de diseño.
El error frecuente
Un error común y peligroso es capturar excepciones por valor, lo que provoca el slicing de objetos:
// ERROR: Captura por valor
try {
throw DataCorruptedException("Error crítico de datos");
} catch (std::runtime_error e) {
// 'e' es una copia de la base (std::runtime_error).
// Se ha "recortado" la parte de DataCorruptedException.
// e.what() podría no devolver el mensaje esperado si la derivada lo sobreescribiera.
std::cerr << e.what() << std::endl;
}
Si lanzas una clase personalizada que hereda de std::runtime_error y la capturas por valor, la instancia de la excepción se convierte en una instancia de la clase base al entrar en el bloque catch. Para que el polimorfismo funcione, la captura debe ser siempre por referencia: catch (const std::runtime_error& e). Además, nunca lances una excepción desde un destructor; esto causará que std::terminate sea llamado si la excepción se propaga durante el “stack unwinding” de otra excepción, abortando el programa inmediatamente.
N° 65