Excepciones: throw, try, catch
Comprende el mecanismo de excepciones de C++ — desenrollado de pila, la jerarquia estandar de excepciones, excepciones personalizadas y cuando preferir excepciones sobre codigos de error.
Por Que Existen las Excepciones
Antes de las excepciones, el codigo C senalaba errores devolviendo valores especiales (como -1 o NULL). Esto tenia un defecto critico: los llamadores podian ignorar silenciosamente el error. Las excepciones resuelven esto forzando al programa a manejar el error o terminar. Tambien separan el sitio de deteccion del error (profundo en el codigo de biblioteca) del sitio de manejo del error (en el codigo de aplicacion), que puede estar a muchos marcos de llamada de distancia.
Las excepciones de C++ siguen un modelo de "costo cero en el camino exitoso" en la mayoria de los compiladores modernos (usando desenrollado basado en tablas). Cuando no se lanza ninguna excepcion, no hay sobrecarga en tiempo de ejecucion — no hay verificaciones if ocultas, no hay predicciones de bifurcacion fallidas. El costo se paga solo cuando una excepcion es realmente lanzada, lo que hace que las excepciones sean ideales para condiciones de error excepcionales (raras).
throw, try y catch
El mecanismo central es sencillo: lanza un objeto para senalar un error, envuelve el codigo riesgoso en un bloque try, y captura los tipos de excepcion coincidentes.
#include <iostream>
#include <stdexcept>
#include <string>
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
throw std::invalid_argument("denominator cannot be zero");
}
return numerator / denominator;
}
int main() {
try {
double result = divide(10.0, 0.0);
std::cout << "Result: " << result << '\n';
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
} catch (...) {
std::cerr << "Unknown error occurred\n";
}
}Desenrollado de Pila en Detalle
Cuando se lanza una excepcion, el runtime realiza el desenrollado de pila: recorre la pila de llamadas hacia atras, destruyendo todas las variables locales en cada marco llamando a sus destructores. Por eso RAII (Adquisicion de Recursos es Inicializacion) es tan crucial — si mantienes recursos en envoltorios RAII, se liberan automaticamente durante el desenrollado.
El proceso de desenrollado funciona de la siguiente manera:
1. La expresion throw crea el objeto de excepcion (generalmente en una area especial de memoria, no en la pila).
2. El runtime busca en la funcion actual un manejador catch coincidente.
3. Si no se encuentra ninguno, sale de la funcion actual, destruyendo todos los objetos locales en orden inverso de construccion.
4. Repite los pasos 2-3 subiendo por la pila de llamadas.
5. Si no se encuentra ningun manejador en ningun marco, se llama a std::terminate().
Regla clave: lanza por valor, captura por referencia const. Lanzar por valor evita punteros colgantes. Capturar por referencia const evita el rebanado (donde una excepcion derivada se copia en un objeto de clase base, perdiendo los datos derivados) y evita copias innecesarias.
La Jerarquia Estandar de Excepciones
La biblioteca estandar proporciona una jerarquia con raiz en std::exception. Comprenderla te ayuda a lanzar el tipo correcto y capturar al nivel adecuado.
// Standard exception hierarchy (simplified)
//
// std::exception
// ├── std::logic_error (programmer mistakes - precondition violations)
// │ ├── std::invalid_argument
// │ ├── std::domain_error
// │ ├── std::length_error
// │ └── std::out_of_range
// ├── std::runtime_error (errors detectable only at runtime)
// │ ├── std::range_error
// │ ├── std::overflow_error
// │ ├── std::underflow_error
// │ └── std::system_error (OS-level errors with error_code)
// └── std::bad_alloc (memory allocation failure)
// std::bad_cast (failed dynamic_cast)
// std::bad_typeid (typeid on null pointer)
#include <stdexcept>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
try {
// .at() throws std::out_of_range, unlike operator[] which is UB
int val = v.at(10);
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << '\n';
}
}Clases de Excepcion Personalizadas
Deriva de std::exception o una de sus subclases para crear excepciones especificas del dominio. Siempre sobrescribe what() para proporcionar un mensaje significativo.
#include <stdexcept>
#include <string>
#include <iostream>
class FileParseError : public std::runtime_error {
public:
FileParseError(const std::string& filename, int line, const std::string& msg)
: std::runtime_error(
"Parse error in '" + filename + "' at line " +
std::to_string(line) + ": " + msg),
filename_(filename),
line_(line) {}
const std::string& filename() const noexcept { return filename_; }
int line() const noexcept { return line_; }
private:
std::string filename_;
int line_;
};
void parse_config(const std::string& filename) {
// Simulate a parse failure at line 42
throw FileParseError(filename, 42, "unexpected token '@@'");
}
int main() {
try {
parse_config("app.conf");
} catch (const FileParseError& e) {
std::cerr << e.what() << '\n';
std::cerr << " File: " << e.filename()
<< ", Line: " << e.line() << '\n';
}
}noexcept y Especificaciones de Excepcion
El especificador noexcept le dice al compilador (y a los lectores humanos) que una funcion nunca lanzara una excepcion. Si una funcion noexcept lanza, se llama a std::terminate() inmediatamente — no hay desenrollado de pila.
Por que importa noexcept?
- Rendimiento: El compilador puede omitir las tablas de desenrollado para funciones noexcept, produciendo codigo mas compacto.
- Optimizacion de la STL: Contenedores como std::vector moveran elementos durante la reasignacion solo si el constructor de movimiento es noexcept. De lo contrario, recurren a copiar para mantener la garantia fuerte de excepcion. Esta puede ser una diferencia dramatica de rendimiento.
- Documentacion: Es un contrato con los llamadores de que la funcion no puede fallar.
Usa noexcept en: constructores de movimiento, operadores de asignacion de movimiento, funciones swap, destructores (implicitamente noexcept), getters simples y operadores de comparacion.
La vieja especificacion throw() (y throw(type-list)) fue obsoleta en C++11 y eliminada en C++20. Siempre usa noexcept en su lugar.
Nunca lances excepciones en destructores. Durante el desenrollado de pila desde otra excepcion, lanzar una segunda excepcion llama a std::terminate(). Si un destructor debe realizar una operacion que puede fallar, captura la excepcion dentro del destructor y manejala ahi (por ejemplo, registrar y suprimir).
- Capturar por valor rebana las excepciones derivadas — siempre captura por
const& - Lanzar punteros (por ejemplo,
throw new std::runtime_error(...)) produce fugas de memoria — lanza por valor - Capturar
...primero oculta todos los manejadores especificos debajo — ponlo al final - Excepciones en destructores durante el desenrollado de pila causan
std::terminate() - Usar excepciones para flujo de control (caminos sin error) anula el modelo de costo cero y dania el rendimiento
- Lanza por valor, captura por referencia
constpara evitar rebanado y fugas - El desenrollado de pila destruye objetos locales en orden inverso de construccion — RAII asegura la limpieza
- Las excepciones de C++ son de costo cero en el camino exitoso (desenrollado basado en tablas), costosas solo cuando se lanzan
noexceptpermite optimizaciones del compilador y es critico para operaciones de movimiento usadas por la STL- Deriva excepciones personalizadas de
std::runtime_errorostd::logic_errory sobrescribewhat()
Quiz — Pon a prueba tus conocimientos
(15 XP)1. Por que deberias capturar excepciones por `const&` en lugar de por valor?
2. Que sucede si una funcion marcada como `noexcept` lanza una excepcion?
3. Durante el desenrollado de pila, que sucede con las variables locales en cada marco de pila?