Estrategias de Manejo de Errores: Elegir el Enfoque Correcto
Compara excepciones, codigos de error y std::expected — aprende cuando cada enfoque es apropiado y como manejar errores en constructores, destructores y codigo multihilo.
No Existe una Solucion Unica
C++ ofrece multiples mecanismos de manejo de errores, cada uno con diferentes compromisos. Elegir el correcto depende de tu dominio:
Excepciones son mejores para errores que son raros y no pueden manejarse localmente. Tienen costo cero en el camino exitoso pero son costosas cuando se lanzan ($1000s de nanosegundos). Se propagan automaticamente a traves de los marcos de llamada.
Codigos de error (valores de retorno, errno, std::error_code) son mejores para errores que son esperados y frecuentes. Tienen un costo pequeno y constante en cada llamada (el llamador debe verificar el valor de retorno). Son el estandar en APIs de C y codigo a nivel de kernel.
std::expected (C++23) combina lo mejor de ambos: fuerza al llamador a reconocer el error (como los codigos de error) pero lleva un tipo de error rico (como las excepciones) y soporta encadenamiento monadico. Se esta convirtiendo rapidamente en el enfoque preferido en codigo C++ nuevo.
La idea clave: estas no son filosofias competidoras — son herramientas para diferentes situaciones, y el codigo de produccion a menudo usa las tres.
Comparacion: Excepciones vs Codigos de Error vs std::expected
| Criterio | Excepciones | Codigos de Error | std::expected |
|---|---|---|---|
| Costo en exito | Cero (basado en tablas) | Bifurcacion + verificacion | Minimo (almacena valor) |
| Costo en error | Alto (desenrollado) | Igual que exito | Igual que exito |
| Se puede ignorar? | No (el programa termina) | Si (errores silenciosos) | Mas dificil (debe acceder .value()) |
| Constructores | Unica opcion | No puede devolver valor | No puede devolver valor |
| Destructores | No debe lanzar | Bien | Bien |
| Componibilidad | Limitada (try/catch) | Cadenas manuales de if | Monadica (and_then, transform) |
| Tamano binario | Mayor (tablas de desenrollado) | Minimo | Minimo |
| Seguro para tiempo real? | No (tiempo no acotado) | Si | Si |
Manejo de Errores en Constructores
Los constructores no pueden devolver codigos de error — no tienen valor de retorno. Las excepciones son el unico mecanismo limpio para senalar fallo de construccion. Una excepcion lanzada significa que el objeto nunca fue completamente construido, por lo que su destructor nunca se llama (pero los destructores de sub-objetos y bases ya construidos si se llaman).
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>
class ConfigFile {
public:
explicit ConfigFile(const std::string& path)
: path_(path), stream_(path) {
if (!stream_.is_open()) {
throw std::runtime_error(
"Cannot open config file: " + path);
}
// Parse the file — if this throws, stream_ is
// already constructed and its destructor will
// close the file automatically (RAII)
parse();
}
std::string get(const std::string& key) const {
auto it = data_.find(key);
if (it == data_.end()) {
throw std::out_of_range("Key not found: " + key);
}
return it->second;
}
private:
void parse() {
std::string line;
while (std::getline(stream_, line)) {
auto pos = line.find('=');
if (pos != std::string::npos) {
data_[line.substr(0, pos)] = line.substr(pos + 1);
}
}
}
std::string path_;
std::ifstream stream_; // RAII: closed by destructor
std::map<std::string, std::string> data_;
};
// Usage: the object is fully valid or does not exist
int main() {
try {
ConfigFile cfg("/etc/myapp.conf");
std::cout << cfg.get("port") << '\n';
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}std::error_code para Errores a Nivel de Sistema
std::error_code proporciona un mecanismo ligero y sin lanzamiento para errores a nivel de sistema. La biblioteca es un gran ejemplo — la mayoria de las funciones tienen una sobrecarga que toma un parametro std::error_code& en lugar de lanzar.
#include <filesystem>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
// Throwing version — suitable for unexpected errors
void copy_file_throwing(const fs::path& src, const fs::path& dst) {
fs::copy_file(src, dst); // throws fs::filesystem_error on failure
}
// Non-throwing version — suitable for expected failures
bool copy_file_safe(const fs::path& src, const fs::path& dst) {
std::error_code ec;
fs::copy_file(src, dst, ec); // sets ec instead of throwing
if (ec) {
std::cerr << "Copy failed: " << ec.message()
<< " (" << ec.category().name()
<< ':' << ec.value() << ")\n";
return false;
}
return true;
}
int main() {
// The error_code version is preferable in loops where
// failure is expected (e.g., batch file processing)
for (const auto& entry : fs::directory_iterator(".")) {
std::error_code ec;
auto sz = fs::file_size(entry.path(), ec);
if (!ec) {
std::cout << entry.path().filename()
<< ": " << sz << " bytes\n";
}
}
}Los destructores en C++11 y posteriores son implicitamente noexcept. Si un destructor lanza, se llama a std::terminate(). Incluso si marcas explicitamente un destructor como noexcept(false), lanzar durante el desenrollado de pila (cuando otra excepcion ya esta activa) llama a std::terminate().
- Registrar y suprimir: Si una operacion de limpieza falla, registra el error y continua
- Proporciona un metodo
close()separado: Permite que los llamadores manejen explicitamente los errores de limpieza antes de la destruccion - Disena recursos para que la limpieza no pueda fallar: Por ejemplo, cerrar un descriptor de archivo siempre tiene exito a nivel del sistema operativo
- Nunca llames a una funcion que lanza desde un destructor sin envolverla en un try/catch
Manejo de Errores en Codigo Multihilo
Las excepciones no cruzan limites de hilo. Si un hilo lanza y la excepcion no es capturada dentro de ese hilo, se llama a std::terminate(). Para propagar excepciones entre hilos:
1. Usa std::future y std::async: Las excepciones lanzadas en la tarea asincrona se capturan y se relanzan cuando llamas a .get() en el future.
2. Usa std::exception_ptr con std::current_exception() y std::rethrow_exception() para capturar y transportar excepciones manualmente.
3. Usa std::promise::set_exception() para enviar una excepcion desde un hilo productor a un consumidor.
Para codigo concurrente de alto rendimiento, los codigos de error o std::expected a menudo se prefieren porque evitan el costo de la infraestructura de excepciones en cada hilo y son mas simples de razonar a traves de limites de hilo.
Empareja el mecanismo de manejo de errores con la situacion.
- Usa excepciones para errores que son raros, no pueden manejarse localmente y deben propagarse hacia arriba en la pila (por ejemplo, archivo no encontrado, base de datos caida)
- Usa codigos de error para operaciones de alta frecuencia donde el fallo es normal y esperado (por ejemplo, fallo de busqueda en cache, E/S no bloqueante que retornaria bloqueo)
- Usa
std::expectedpara codigo nuevo donde quieras manejo explicito de errores con tipos de error ricos y composicion monadica - Usa excepciones en constructores — son la unica forma de senalar fallo antes de que el objeto exista
- Nunca lances en destructores — registra el error y suprimelo
- Las excepciones, codigos de error y
std::expectedson herramientas complementarias — no filosofias en competencia - Los constructores deben usar excepciones para senalar fallo; los codigos de error no son una opcion
- Los destructores nunca deben lanzar — son implicitamente
noexceptdesde C++11 - Las excepciones no cruzan limites de hilo — usa
std::futureostd::exception_ptrpara transferirlas std::error_codees ideal para errores a nivel de sistema donde quieres una API sin lanzamiento
Quiz — Pon a prueba tus conocimientos
(15 XP)1. Por que las excepciones son el mecanismo preferido de manejo de errores en constructores?
2. Que sucede si se lanza una excepcion en un destructor durante el desenrollado de pila desde otra excepcion?
3. Como se pueden transferir excepciones entre hilos?