DMarlon Studio

Lección 6 · 6 de 20

RAII y destructores · C++

Libere recursos sin olvidos con RAII y destructores en C++: una conexión que se cierra sola y una transacción que se deshace si hay una excepción.

C++ · RAII y destructores en DMarlon Studio: el código del ejemplo y su salida en la terminal

El destructor es el método que lleva el nombre de la clase precedido de ~, como ~DbConnection(). No tiene parámetros y C++ lo llama solo cuando el objeto deja de existir: al salir de su bloque { }, al terminar la función o al destruirse el objeto que lo contiene.

RAII (*Resource Acquisition Is Initialization*) aprovecha esa garantía: el constructor adquiere un recurso (una conexión, un archivo, un bloqueo) y el destructor lo libera. Quien usa la clase no tiene que acordarse de cerrar nada: DbConnection db("bank.db"); abre, y el cierre llega solo. Así funcionan std::ofstream, std::vector o std::lock_guard.

Transaction va un paso más allá: su destructor mira committed_ y, si nadie llamó a commit(), deshace los cambios. En transfer(200) todo sale bien: se confirma y luego se cierra la conexión. Los objetos locales se destruyen en orden inverso al de su creación: primero tx, después db.

En transfer(900) se lanza una excepción antes del commit(). Mientras la excepción sale de la función, C++ destruye los objetos locales (lo que se llama *stack unwinding*): la transacción se deshace, la conexión se cierra y solo entonces el catch de main escribe el error. Con RAII no hace falta repetir la limpieza en cada camino de salida.

Las aplicaciones de escritorio con FLTK que genera DMarlon Studio siguen este mismo patrón con ventanas y recursos. Este ejemplo, como todos los del curso, se ejecuta con el destino Consola pulsando F9.

Código · main.cpp

#include <iostream>#include <stdexcept>#include <string>// RAII: the constructor acquires a resource and the destructor releases it.class DbConnection {    std::string name_;public:    explicit DbConnection(std::string name) : name_(name) { std::cout << "open  " << name_ << "\n"; }    ~DbConnection() { std::cout << "close " << name_ << "\n"; }    // destructor};// A transaction rolls back by itself unless commit() was called.class Transaction {    bool committed_ = false;public:    Transaction() { std::cout << "  begin\n"; }    void commit() { committed_ = true; std::cout << "  commit\n"; }    ~Transaction() { if (!committed_) std::cout << "  rollback\n"; }};void transfer(int amount) {    DbConnection db("bank.db");    Transaction tx;    if (amount > 500) throw std::runtime_error("limit exceeded");    tx.commit();}   // leaving the scope: tx is destroyed first, then db (reverse order)int main() {    transfer(200);    try { transfer(900); }    // the exception leaves transfer(), but the destructors run    catch (const std::exception& e) { std::cout << "Error: " << e.what() << "\n"; }}

Salida · c++ -std=c++17 main.cpp -o main && ./main

open  bank.db
  begin
  commit
close bank.db
open  bank.db
  begin
  rollback
close bank.db
Error: limit exceeded

Para recordar

  • El destructor, ~Clase(), se llama solo cuando el objeto deja de existir.
  • RAII: el constructor adquiere el recurso y el destructor lo libera.
  • Los objetos locales se destruyen en orden inverso al de creación.
  • Los destructores también se ejecutan cuando una excepción sale de la función.
  • Un destructor no debe lanzar excepciones.

Ejercicio

Añada una clase FileLock que escriba lock al crearse y unlock al destruirse, y cree uno en transfer antes de db. Mire en qué orden aparecen los mensajes en los dos casos.