RAII (Resource Acquisition Is Initialization) es un patrón de diseño que vincula la adquisición de un recurso a la vida de un objeto en el stack. En lugar de gestionar manualmente la liberación de un recurso (como llamar a free, fclose o delete), delegas esa responsabilidad al destructor de un objeto.
El mecanismo es elegante: el lenguaje C++ garantiza que, cuando un objeto local sale de su ámbito, su destructor se ejecute automáticamente. Esto ocurre tanto en una salida normal del flujo como durante el stack unwinding (desenrollado de la pila) que ocurre cuando se lanza una excepción.
Debes usar este patrón siempre que gestiones algo que no sea memoria gestionada automáticamente por el compilador: un file handle, un mutex, un socket o una transacción de base de datos. Si intentas gestionar estos recursos manualmente mediante if (error) { cleanup(); return; }, terminarás con un código propenso a errores, especialmente cuando el flujo de ejecución se vuelve complejo o se introducen excepciones.
Si ignoras este principio y gestionas recursos manualmente, cualquier excepción que se lance antes de la línea de liberación del recurso provocará una fuga (leak). El recurso quedará bloqueado o la memoria ocupada de forma indefinida, lo que en sistemas de alta disponibilidad o servicios de larga duración derivará en un fallo catastrófico por agotamiento de recursos.
#include <iostream>
#include <cstdio>
#include <stdexcept>
#include <utility>
#include <string>
// Clase RAII que envuelve un recurso de C (FILE*)
class ScopedFile {
public:
// El constructor adquiere el recurso
explicit ScopedFile(const std::string& filename, const std::string& mode) {
handle_ = std::fopen(filename.c_str(), mode.c_str());
if (!handle_) {
throw std::runtime_error("No se pudo abrir el archivo.");
}
std::cout << "[Log] Archivo abierto: " << filename << "\n";
}
// El destructor garantiza la liberación del recurso
~ScopedFile() {
if (handle_) {
std::fclose(handle_);
std::cout << "[Log] Archivo cerrado automáticamente por destructor.\n";
}
}
// Prohibimos la copia para evitar la doble liberación (double-free)
ScopedFile(const ScopedFile&) = delete;
ScopedFile& operator=(const ScopedFile&) = delete;
// Implementamos semántica de movimiento para transferir la propiedad
ScopedFile(ScopedFile&& other) noexcept : handle_(other.handle_) {
other.handle_ = nullptr;
}
ScopedFile& operator=(ScopedFile&& other) noexcept {
if (this != &other) {
if (handle_) {
std::fclose(handle_);
}
handle_ = other.handle_;
other.handle_ = nullptr;
}
return *this;
}
void write(const std::string& text) {
if (!handle_) {
throw std::runtime_error("Intento de escribir en un archivo cerrado.");
}
std::fputs(text.c_str(), handle_);
}
private:
std::FILE* handle_ = nullptr;
};
void process_with_error() {
ScopedFile file("datos.txt", "w");
file.write("Línea de datos crítica.\n");
// Simulamos un error inesperado
throw std::runtime_error("Error de hardware simulado");
// El destructor de 'file' se llamará aquí debido al stack unwinding
// incluso si nunca llegamos a esta línea.
}
int main() {
try {
process_with_error();
} catch (const std::exception& e) {
std::cerr << "[Catch] Error capturado en main: " << e.what() << "\n";
}
std::cout << "Fin del programa.\n";
return 0;
}
En el ejemplo anterior, la clase ScopedFile encapsula un puntero crudo de C (std::FILE*). El constructor realiza la adquisición; si std::fopen falla, la excepción impide que se cree un objeto en un estado inválido. El destructor es el componente crítico: si el objeto llega al final de su vida (ya sea por fin de función o por una excepción), se llama a std::fclose.
Para evitar el error de intentar cerrar el mismo archivo dos veces, hemos marcado los constructores de copia con = delete [C++11]. Esto impide que el compilador genere una copia superficial que dejaría dos objetos con el mismo handle_. En su lugar, implementamos la semántica de movimiento: el constructor de movimiento transfiere el puntero al nuevo objeto y anula el original (other.handle_ = nullptr), asegurando que solo un objeto sea dueño del recurso en todo momento.
Cuando process_with_error lanza una excepción, el runtime de C++ comienza el proceso de stack unwinding. El objeto file es destruido mientras la pila se va desarmando, lo que garantiza que el archivo se cierre correctamente antes de que la excepción llegue al bloque catch en main.
El error frecuente
Lanzar una excepción desde un destructor mientras la pila ya se está desenrollando debido a otra excepción previa.
~ScopedFile() {
if (algo_mal) {
throw std::runtime_error("Error en destructor"); // ¡ERROR FATAL!
}
}
Si una excepción está en curso (buscando un manejador de catch) y un destructor llamado durante el stack unwinding también lanza una excepción, el programa no tiene una forma unívoca de gestionar dos excepciones simultáneas. El estándar dicta que el runtime debe llamar inmediatamente a std::terminate(), abortando el proceso sin posibilidad de recuperación. Los destructores deben ser noexcept por defecto para evitar este comportamiento catastrófico.
N° 63