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_to 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. longjmpcruzando 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_lockystd::fstreamcubren 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/deletedesnudos. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines