Manejo de errores en C++: de try/catch a RAII y noexcept
La fiabilidad de un programa depende directamente de cómo reacciona ante situaciones imprevistas. En C++, el mecanismo principal para esto son las excepciones. En este artículo, analizaremos la construcción clásica try/catch/throw, así como enfoques alternativos y modernos: RAII, noexcept y el manejo de errores sin excepciones.
1. Mecanismo básico: try, catch, throw
Una excepción es un objeto que se "lanza" (throw) cuando ocurre un error. El programa puede capturarlo (catch) y realizar acciones correctivas sin interrumpir completamente el funcionamiento.
Sintaxis
#include <iostream>#include <stdexcept>
double divide(double a, double b) { if (b == 0.0) { throw std::runtime_error("¡División por cero!"); } return a / b;}
int main() { try { double result = divide(10.0, 0.0); std::cout << "Resultado: " << result << std::endl; } catch (const std::runtime_error& e) { std::cerr << "Error: " << e.what() << std::endl; } return 0;}Reglas de funcionamiento
- throw — crea una copia del objeto y transfiere el control al manejador.
- try — bloque de código donde pueden ocurrir excepciones.
- catch — captura la excepción por tipo. Se pueden especificar múltiples bloques
catch. - Si la excepción no es capturada, se llama a
std::terminate().
Captura por clase base
Todas las excepciones estándar heredan de std::exception. Por lo tanto, se puede escribir un manejador universal:
try { // código} catch (const std::exception& e) { std::cerr << "Error estándar: " << e.what() << std::endl;} catch (...) { std::cerr << "Excepción desconocida" << std::endl;}El bloque catch (...) captura absolutamente todas las excepciones, pero sin acceso al objeto.
2. Excepciones en constructores y destructores
Constructores
Si un constructor lanza una excepción, se considera que el objeto no ha sido creado y el destructor para él no se llama. Esto es importante para la gestión de recursos. Ejemplo:
class Resource {public: Resource() { std::cout << "Recurso asignado" << std::endl; throw std::runtime_error("Error de inicialización"); } ~Resource() { std::cout << "Recurso liberado" << std::endl; }};
int main() { try { Resource r; } catch (...) { std::cout << "Excepción capturada" << std::endl; } // ¡El destructor de Resource NO será llamado! return 0;}Destructores
Lanzar excepciones desde un destructor es una mala práctica. Si el destructor se llama durante el desenrollado de la pila (stack unwinding) y lanza una nueva excepción, el programa terminará abruptamente. Siempre escriba los destructores como noexcept (por defecto ya lo son).
3. RAII — gestión automática de recursos
El idiom RAII (Resource Acquisition Is Initialization) es la clave para un código seguro. El recurso (memoria, archivo, mutex) se adquiere en el constructor y se libera en el destructor. Si ocurre una excepción, los destructores de todos los objetos creados se llaman automáticamente (stack unwinding) y los recursos se liberan.
Ejemplo con el puntero inteligente std::unique_ptr:
#include <memory>#include <iostream>
void process() { auto ptr = std::make_unique<int>(42); // Si aquí ocurre una excepción, // unique_ptr liberará la memoria automáticamente throw std::runtime_error("Error en proc");
```