Manejo de errores en C++: de try/catch a enfoques modernos
\n\nEl mecanismo de excepciones en C++ es una herramienta potente, pero que requiere un profundo entendimiento. Mientras que el uso básico de try/catch es conocido por la mayoría de los desarrolladores, el manejo profesional de errores incluye mucho más: desde garantías de seguridad de excepciones hasta estrategias alternativas sin excepciones. En este artículo analizaremos técnicas avanzadas para trabajar con errores en C++ moderno.
1. Garantías de seguridad de excepciones: el fundamento del código robusto
\n\nCualquier función que pueda lanzar una excepción debe proporcionar una de las tres garantías de seguridad:
\n\n- \n
- Garantía Básica (Basic Guarantee): en caso de excepción, los invariantes del objeto se mantienen, no hay fugas de recursos. El objeto puede estar en un estado impredecible pero válido. \n
- Garantía Fuerte (Strong Guarantee): la operación se completa completamente o el estado del programa permanece sin cambios (atomicidad). Se logra mediante el patrón copy-and-swap. \n
- Garantía de ausencia de excepciones (Noexcept Guarantee): la función nunca lanza excepciones. Es crítico para destructores, operaciones move y operaciones utilizadas en contenedores. \n
// Ejemplo de garantía fuerte mediante copy-and-swap\nclass Vector {\n int* data;\n size_t sz, cap;\npublic:\n void push_back(int value) {\n if (sz == cap) {\n // 1. Creamos una copia\n Vector tmp(*this);\n // 2. Modificamos la copia (puede lanzar excepción)\n tmp.reserve(cap * 2);\n // 3. Intercambio (¡noexcept!)\n swap(tmp);\n }\n data[sz++] = value; // noexcept\n }\n \n void swap(Vector& other) noexcept {\n std::swap(data, other.data);\n std::swap(sz, other.sz);\n std::swap(cap, other.cap);\n }\n};\n\n2. RAII como base de la gestión de recursos
\n\nEl patrón RAII (Resource Acquisition Is Initialization) no son solo punteros inteligentes. Es un principio fundamental que permite evitar fugas de recursos en caso de excepciones:
\n\n- \n
- El recurso se adquiere en el constructor \n
- Se libera en el destructor (que por defecto es
noexcept) \n - Una excepción en el destructor es UB (comportamiento indefinido) \n
// Envoltorio RAII para un descriptor de archivo\nclass FileDescriptor {\n int fd;\npublic:\n explicit FileDescriptor(const char* path) \n : fd(open(path, O_RDONLY)) {\n if (fd == -1) throw std::runtime_error("Error al abrir el archivo");\n }\n \n ~FileDescriptor() noexcept {\n if (fd != -1) close(fd);\n }\n \n // Prohibimos copia, permitimos move\n FileDescriptor(FileDescriptor&& other) noexcept : fd(other.fd) {\n other.fd = -1;\n }\n};\n\n3. Alternativas modernas a las excepciones: std::optional y std::expected
\n\nNo todos los errores deben manejarse mediante excepciones. Para errores esperados (entrada inválida, falta de datos) es mejor usar tipos envoltorio:
\n\n#include <optional>\n#include <expected> // C++23\n\n// std::optional — para valores opcionales\nstd::optional<int> safe_divide(int a, int b) {\n if (b == 0) return std::nullopt;\n return a / b;\n}\n\n// std::expected — para errores con información (C++23)\nstd::expected<double, std::string> sqrt_checked(double x) {\n if (x < 0) return std::unexpected("Entrada negativa");\n return std::sqrt(x);\n}\n\n// Uso\nauto result = safe_divide(10, 0);\nif (result) {\n std::cout << *result;\n} else {\n std::cout << "División por cero";\n}\n\n4. noexcept: especificación y optimizaciones
\n\nEl especificador noexcept no es solo documentación. Afecta al rendimiento y a