conceptC++ Moderno~4 min de lecturaActualizado 2026-07-02#cpp#raii#destructors#exceptions#ownership

RAII: la idea que cambia todo

RAII significa Resource Acquisition Is Initialization: adquirí un recurso en un constructor, liberalo en el destructor y dejá que el lifetime del objeto maneje el cleanup. El recurso puede ser heap memory, un file descriptor, un lock, un socket, una región mapeada, un directorio temporal o cualquier operación que "hay que deshacer". La clave es que el cleanup pasa a ser una propiedad del tipo, no algo que cada caller tiene que recordar en cada salida. Cuando internalizás RAII, la mayor parte del diseño C++ moderno es variación de esta idea.

El reset: en C, cleanup suele ser un problema de control flow. En C++, cleanup debería ser un problema de diseño de tipos.

Cómo funciona de verdad

Un lifetime de objeto tiene un comienzo y un final. En C++, los constructores corren al comienzo, los destructores corren al final y los subobjetos se destruyen automáticamente. Para objetos con automatic storage, el final es la salida del scope. Para objetos dinámicos, el final es delete o el destructor del wrapper RAII owning. Para temporarios, el final suele ser la full expression. Durante stack unwinding por una excepción, los destructores de objetos automáticos completamente construidos también corren.

Eso le da a C++ un cleanup stack incorporado:

Evento Qué hace C++
El constructor termina bien empezó el lifetime del objeto
El constructor lanza los subobjetos ya construidos se destruyen
El scope termina normalmente los objetos automáticos se destruyen en orden inverso de construcción
Una excepción sale del scope los objetos automáticos se destruyen durante unwinding
Corre delete p corre el destructor y después se libera storage
Muere un miembro RAII su propio destructor libera su recurso

Los tipos RAII suelen seguir una regla simple de ownership: copiá solo si duplicar ownership es válido, mové si el ownership puede transferirse, borrá copy si dos owners causarían double-release. Un std::lock_guard<std::mutex> no es copyable porque dos lock guards no pueden poseer la misma adquisición de lock. Un std::unique_ptr<T> es move-only porque el ownership se puede transferir pero no duplicar. Un std::vector<T> es copyable porque copiar significa crear un buffer distinto y copiar elementos.

A nivel máquina, RAII no es un runtime de fondo. El compilador emite llamadas a destructores en cada camino de salida de scope que tiene que preservar. Con excepciones habilitadas, también emite metadata de unwinding y caminos de cleanup para que los destructores corran cuando el control sale por un throw. Si compilás el demo con -S, podés ver esas llamadas y las tablas extra de exception handling. Si compilás sin excepciones, RAII sigue funcionando para salidas normales de scope, pero tirar excepciones a través del código deja de ser un camino soportado.

Artefacto ejecutable: cleanup en éxito y falla

El demo ejecutable vive en examples/modern-cpp/raii-the-idea-that-changes-everything/. Compara una función C con una label cleanup: contra una función C++ donde TempFile y ByteBuffer se liberan solos.

cd examples/modern-cpp/raii-the-idea-that-changes-everything
./run.sh

El lado C tiene que hacer pasar cada edición futura por la misma disciplina de cleanup:

FILE *file = tmpfile();
char *buffer = malloc(64);

if (fail_after_open) {
    goto cleanup;
}

cleanup:
    free(buffer);
    if (file != NULL) {
        fclose(file);
    }

El lado C++ mueve el protocolo a los tipos owning del recurso:

class TempFile {
public:
    TempFile() : file_(std::tmpfile()) {
        if (file_ == nullptr) {
            throw std::runtime_error("tmpfile failed");
        }
    }

    TempFile(const TempFile &) = delete;
    TempFile &operator=(const TempFile &) = delete;

    ~TempFile() {
        if (file_ != nullptr) {
            std::fclose(file_);
        }
    }

private:
    FILE *file_;
};

run.sh también emite demo.O2.s:

g++ -std=c++20 -O2 -S demo.cpp -o demo.O2.s

Buscá el camino normal y el camino de cleanup. El destructor es código generado ordinario, pero el source ya no depende de que cada caller recuerde fclose y free en cada rama futura.

Fallas típicas y trade-offs

  • Un destructor no debe fallar hacia afuera. Lanzar desde un destructor durante stack unwinding puede terminar el programa. Los destructores deberían liberar, loguear, setear flags o requerir un paso explícito close()/commit() cuando la falla debe reportarse.
  • RAII necesita ownership claro. Envolver un puntero borrowed en un destructor owning causa double-free. Guardar un puntero owning crudo sin destructor causa leak.
  • release() es filoso. std::unique_ptr::release() te entrega el puntero crudo y deja de borrarlo. Después de eso, volviste a ownership manual.
  • Las APIs C necesitan adapters. Un handle C como FILE*, DIR*, pthread_mutex_t o un file descriptor se vuelve RAII recién cuando lo envolvés en un tipo con el destructor correcto.
  • La terminación de proceso puede saltear destructores. _Exit, std::quick_exit, terminación anormal y algunos caminos de signal no corren cleanup normal de stack.
  • longjmp cruzando frames C++ es una trampa. No saltes por encima de objetos con destructores no triviales; usá excepciones C++ o mantené el borde en código solo C.

En la práctica

  • Adquirí en constructores, liberá en destructores. Evitá inicialización en dos fases salvo que la segunda fase tenga una razón fuerte.
  • Hacé que los estados inválidos no sean representables. Un objeto RAII construido debería poseer un recurso válido o fallar al construirse.
  • Borrá copy para recursos únicos. File descriptors, locks y allocations crudas de heap necesitan move semantics o no ser copyable, no copy accidental.
  • Preferí RAII estándar primero. std::vector, std::string, std::unique_ptr, std::lock_guard, std::scoped_lock y std::fstream cubren los casos comunes.
  • Usá labels de cleanup en C, RAII en bordes C++. Los dos estilos pueden convivir, pero no dejes ownership ambiguo entre ellos.

Conecta con: C vs C++: qué ganás de verdad (y qué pagás) · Smart pointers: unique_ptr, shared_ptr, ownership · Move semantics y value categories · Memory leaks y disciplina de ownership · Use-after-free y double-free · Stack vs heap · Sanitizers: ASan, UBSan y TSan

Fuentes

  • cppreference - RAII - definición compacta de resource acquisition, release por destructor y cleanup exception-safe. https://en.cppreference.com/w/cpp/language/raii
  • cppreference - Destructors - cuándo se invocan destructores, incluyendo salida de scope, delete, temporarios y stack unwinding. https://en.cppreference.com/w/cpp/language/destructor
  • cppreference - Exceptions - modelo de stack unwinding y mecánica de exception handling. https://en.cppreference.com/w/cpp/language/exceptions
  • C++ working draft - [class.dtor] - wording estándar para destructores y destrucción. https://eel.is/c++draft/class.dtor
  • C++ working draft - [except.ctor] - comportamiento de construcción, destrucción y unwinding cuando constructores lanzan. https://eel.is/c++draft/except.ctor
  • C++ Core Guidelines - reglas de resource management como administrar recursos automáticamente y evitar new/delete desnudos. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines