Ir al contenido

Garantias de Seguridad de Excepciones

Aprende los tres niveles de seguridad de excepciones — basica, fuerte y sin lanzamiento — y como RAII y copy-and-swap las proporcionan.

Las Tres Garantias

La seguridad de excepciones no se trata de prevenir excepciones — se trata de definir que garantiza tu codigo cuando ocurre una excepcion. Cada funcion que escribes implicitamente proporciona uno de estos niveles:

1. Garantia de no lanzamiento (la mas fuerte): La funcion nunca lanza. Siempre tiene exito. Ejemplos: destructores, swap, getters simples. Marcada como noexcept.

2. Garantia fuerte (commit-or-rollback): Si la funcion lanza, el estado del programa es exactamente el mismo que antes de la llamada. Es como si la funcion nunca hubiera sido llamada. Piensa en una transaccion de base de datos que se confirma completamente o se revierte completamente.

3. Garantia basica (minima aceptable): Si la funcion lanza, el programa esta en un estado valido pero no especificado. No se pierden recursos, todos los invariantes se mantienen, pero el estado exacto puede diferir del que era antes.

Tambien hay un cuarto nivel, inaceptable: sin garantia — donde una excepcion puede dejar el programa en un estado corrupto con recursos perdidos. El codigo de produccion nunca debe proporcionar menos que la garantia basica.

RAII: La Base de la Seguridad de Excepciones

RAII (Adquisicion de Recursos es Inicializacion) vincula el tiempo de vida de un recurso al tiempo de vida de un objeto. El constructor adquiere el recurso; el destructor lo libera. Como los destructores se ejecutan durante el desenrollado de pila, RAII proporciona la garantia basica automaticamente.

raii_db.cpp
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>

class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& conn_str)
        : conn_str_(conn_str), connected_(false) {
        // Simulate connection
        if (conn_str.empty()) {
            throw std::invalid_argument("empty connection string");
        }
        connected_ = true;
        std::cout << "Connected to: " << conn_str_ << '\n';
    }

    ~DatabaseConnection() {
        if (connected_) {
            // Cleanup never throws
            std::cout << "Disconnected from: " << conn_str_ << '\n';
            connected_ = false;
        }
    }

    void execute(const std::string& query) {
        if (!connected_) throw std::runtime_error("not connected");
        // Even if this function throws, the destructor
        // will still close the connection during unwinding
        std::cout << "Executing: " << query << '\n';
    }

    // Non-copyable, movable
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    DatabaseConnection(DatabaseConnection&& other) noexcept
        : conn_str_(std::move(other.conn_str_)),
          connected_(other.connected_) {
        other.connected_ = false;
    }

private:
    std::string conn_str_;
    bool connected_;
};

void process_data() {
    DatabaseConnection db("postgres://localhost/mydb");
    db.execute("SELECT * FROM users");
    // If execute() throws, ~DatabaseConnection() still runs
    // — the connection is always properly closed
}

Copy-and-Swap para la Garantia Fuerte

El modismo copy-and-swap proporciona la garantia fuerte de excepcion para operadores de asignacion. La idea: haz una copia primero (que puede lanzar), luego intercambia con la copia (que nunca lanza). Si la copia lanza, el objeto original no se toca.

copy_and_swap.cpp
#include <algorithm>
#include <utility>
#include <cstddef>

class Matrix {
public:
    Matrix(std::size_t rows, std::size_t cols)
        : rows_(rows), cols_(cols),
          data_(new double[rows * cols]{}) {}

    // Copy constructor — may throw (allocates memory)
    Matrix(const Matrix& other)
        : rows_(other.rows_), cols_(other.cols_),
          data_(new double[other.rows_ * other.cols_]) {
        std::copy(other.data_,
                  other.data_ + rows_ * cols_,
                  data_);
    }

    // Move constructor — never throws
    Matrix(Matrix&& other) noexcept
        : rows_(other.rows_), cols_(other.cols_),
          data_(other.data_) {
        other.data_ = nullptr;
        other.rows_ = other.cols_ = 0;
    }

    ~Matrix() { delete[] data_; }

    // No-throw swap — the key ingredient
    friend void swap(Matrix& a, Matrix& b) noexcept {
        using std::swap;
        swap(a.rows_, b.rows_);
        swap(a.cols_, b.cols_);
        swap(a.data_, b.data_);
    }

    // Copy-and-swap assignment: STRONG guarantee
    // Takes parameter by value (invokes copy/move ctor)
    Matrix& operator=(Matrix other) noexcept {
        swap(*this, other);
        return *this;
        // 'other' is destroyed here, freeing old data
    }

private:
    std::size_t rows_, cols_;
    double* data_;
};

noexcept y Operaciones de Movimiento

La STL depende en gran medida de noexcept para los constructores de movimiento. Considera std::vector::push_back: cuando el vector debe reasignar, necesita transferir elementos del buffer antiguo al nuevo.

- Si el constructor de movimiento del elemento es noexcept, el vector mueve los elementos (rapido).
- Si el constructor de movimiento podria lanzar, el vector copia los elementos en su lugar (lento, pero seguro — si una copia lanza, el buffer original permanece intacto).

Por eso marcar los constructores de movimiento como noexcept no es solo buena practica — puede ser una diferencia de rendimiento de 10x o 100x para tipos almacenados en contenedores.

Puedes verificar en tiempo de compilacion con std::is_nothrow_move_constructible_v. La STL usa este trait internamente para decidir su estrategia.

Identificando el Nivel de Garantia

Al escribir una funcion, piensa en que garantia proporciona. Aqui hay un ejemplo de los tres niveles en una sola clase.

guarantee_levels.cpp
#include <vector>
#include <string>
#include <algorithm>
#include <stdexcept>

class UserRegistry {
public:
    // NO-THROW: simple getter, can never fail
    std::size_t size() const noexcept {
        return users_.size();
    }

    // STRONG GUARANTEE: either the user is added or nothing changes
    void add_user(const std::string& name) {
        if (name.empty()) {
            throw std::invalid_argument("name cannot be empty");
        }
        // If push_back throws (e.g., bad_alloc during reallocation),
        // vector guarantees the original data is intact.
        users_.push_back(name);
    }

    // BASIC GUARANTEE: if an exception occurs partway through,
    // some users may have been processed and some may not.
    // But the object is still in a valid state (no leaks, no corruption).
    void normalize_all() {
        for (auto& user : users_) {
            // std::transform could theoretically throw
            // (e.g., if user string allocation fails during tolower)
            std::transform(user.begin(), user.end(), user.begin(),
                          [](unsigned char c) { return std::tolower(c); });
        }
    }

private:
    std::vector<std::string> users_;
};
Buena práctica

Sigue estas directrices para escribir codigo con seguridad de excepciones de forma consistente.

  • Usa RAII en todas partes: Nunca mantengas recursos crudos — envuelvelos en smart pointers o envoltorios RAII
  • Marca los constructores de movimiento y swap como noexcept: Esto habilita optimizaciones de la STL y proporciona la garantia de no lanzamiento
  • Haz todo el trabajo que pueda lanzar antes de modificar el estado: Asigna, calcula y valida primero — luego confirma con operaciones que no lanzan como swap
  • Prefiere el modismo copy-and-swap para operadores de asignacion para obtener la garantia fuerte con esfuerzo minimo
  • Los destructores nunca deben lanzar: Son implicitamente noexcept en C++11 y posteriores
Error común

Estos patrones rompen las garantias de seguridad de excepciones y llevan a errores sutiles.

  • Modificar el estado antes de completar todas las operaciones que lanzan: Si el paso 2 de 3 lanza, el paso 1 ya cambio el objeto
  • Olvidar que new puede lanzar std::bad_alloc: Cada asignacion dinamica es un punto potencial de lanzamiento
  • Constructores de movimiento no-noexcept: La STL recurrira silenciosamente a copiar, matando el rendimiento
  • Capturar excepciones de forma demasiado amplia: catch(...) sin relanzar oculta el problema real
Puntos clave
  • Las tres garantias son: sin lanzamiento (siempre tiene exito), fuerte (commit-or-rollback) y basica (estado valido pero no especificado)
  • RAII es la base — los destructores se ejecutan durante el desenrollado, asegurando que los recursos siempre se liberen
  • Copy-and-swap da la garantia fuerte: haz todo el trabajo que lanza primero, luego swap (que es noexcept)
  • Los constructores de movimiento noexcept son criticos — sin ellos, std::vector recurre a copiar en la reasignacion
  • Cada funcion deberia aspirar al menos a la garantia basica; sin lanzamiento donde sea posible

Quiz — Pon a prueba tus conocimientos

(20 XP)

1. Que promete la garantia fuerte de excepcion?

2. Por que `std::vector` copia elementos en lugar de moverlos cuando el constructor de movimiento no es `noexcept`?

3. En el modismo copy-and-swap, por que el operador de asignacion toma su parametro por valor?