Ir al contenido

RAII y la Regla de Cero/Cinco

Comprende RAII (la Adquisicion de Recursos Es Inicializacion) como el patron central de gestion de recursos de C++, la Regla de Cinco para clases que gestionan recursos, y la Regla de Cero para el resto.

RAII: El Patron Fundamental de C++

RAII (Adquisicion de Recursos Es Inicializacion) es el patron mas importante de C++. La idea es simple pero poderosa:

- Adquiere recursos en el constructor (abre un archivo, asigna memoria, adquiere un lock)
- Libera recursos en el destructor (cierra el archivo, libera la memoria, libera el lock)

Como C++ garantiza que los destructores se llaman cuando los objetos salen del ambito (incluso durante el desenrollado de excepciones), los recursos se liberan automaticamente. No se necesitan bloques finally, no se necesitan sentencias goto cleanup, no se necesita gestion manual de recursos.

RAII convierte la gestion de recursos de un problema en tiempo de ejecucion ("recordar liberar") en una garantia en tiempo de compilacion ("el destructor siempre se ejecuta"). Cada recurso que gestiones deberia estar envuelto en un objeto RAII.

Ejemplos de RAII en la biblioteca estandar: std::unique_ptr, std::shared_ptr, std::lock_guard, std::fstream, std::string, std::vector.

RAII en Accion

Este ejemplo demuestra como RAII maneja automaticamente la limpieza de recursos, incluso cuando se lanzan excepciones.

La Regla de Cinco

Si tu clase gestiona directamente un recurso (puntero crudo, file handle, socket), debes implementar o eliminar las cinco funciones miembro especiales:

1. Destructor — ~T() — limpia el recurso
2. Constructor de copia — T(const T&) — copia profunda del recurso
3. Operador de asignacion por copia — T& operator=(const T&) — copia profunda con limpieza
4. Constructor de movimiento — T(T&&) noexcept — transfiere la propiedad del recurso
5. Operador de asignacion por movimiento — T& operator=(T&&) noexcept — transfiere con limpieza

Si implementas alguno, probablemente necesites los cinco. Si declaras un destructor, el compilador no generara automaticamente las operaciones de movimiento. Si declaras operaciones de copia, las operaciones de movimiento estan suprimidas (no eliminadas — simplemente no se generan).

Implementacion de la Regla de Cinco

Aqui hay una clase completa que implementa la Regla de Cinco para gestionar un buffer crudo. En la practica, usarias std::vector en su lugar.

variadic_forwarding.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Employee {
    std::string name;
    int age;
    Employee(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << '\n';
    }
    Employee(const Employee&) { std::cout << "Copied\n"; }
    Employee(Employee&&) noexcept { std::cout << "Moved\n"; }
};

// Custom factory using variadic forwarding
template <typename T, typename... Args>
std::vector<T> make_vector_of(Args&&... args) {
    std::vector<T> v;
    // Fold expression (C++17) + emplace_back
    (v.emplace_back(std::forward<Args>(args)), ...);
    return v;
}

int main() {
    std::vector<Employee> team;

    // push_back: creates temporary, then MOVES it into vector
    team.push_back(Employee("Alice", 30));
    // Output: Constructed: Alice, Moved

    // emplace_back: constructs IN-PLACE — no copy, no move
    team.emplace_back("Bob", 25);
    // Output: Constructed: Bob

    std::cout << "---\n";
    // Variadic forwarding
    auto v = make_vector_of<std::string>("hello", std::string("world"), "!");
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}
Buena práctica

La Regla de Cero establece que la mayoria de las clases no deberian declarar ninguna funcion miembro especial. En su lugar, componen su comportamiento a partir de miembros que se gestionan a si mismos:

- Usa std::unique_ptr en lugar de punteros crudos con propiedad
- Usa std::string en lugar de char*
- Usa std::vector en lugar de arrays asignados dinamicamente
- Usa std::lock_guard en lugar de lock/unlock manual

Cuando todos los miembros manejan su propia limpieza, el destructor, constructor de copia, operador de asignacion por copia, constructor de movimiento y operador de asignacion por movimiento generados por el compilador hacen lo correcto automaticamente.

La Regla de Cero deberia ser tu estado por defecto. Solo escribe la Regla de Cinco para clases RAII de bajo nivel que envuelven directamente un recurso crudo.

Error común

1. Declarar un destructor suprime las operaciones de movimiento. Si escribes ~MyClass() = default;, las operaciones de movimiento no se generan — tu clase copiara silenciosamente donde esperas que mueva.

2. El idioma copy-and-swap implementa la asignacion por copia y por movimiento en un solo operador: T& operator=(T other) (nota: por valor). El parametro se construye por copia o por movimiento, luego se intercambia con *this. Simple, seguro ante excepciones y correcto.

3. El slicing ocurre cuando copias un objeto derivado a una variable base — la parte derivada se pierde. Usa punteros inteligentes (unique_ptr) para objetos polimorficos.

4. = delete vs = default — = delete hace que llamar a la funcion sea un error de compilacion. = default le pide al compilador que la genere. Usa = delete para hacer las clases no copiables: T(const T&) = delete;.

Mezcla de Regla de Cero y Regla de Cinco

Este ejemplo muestra como una clase de Regla de Cero compone tipos RAII, y como una clase de Regla de Cinco envuelve un recurso crudo.

Puntos clave
  • RAII vincula el tiempo de vida de los recursos al tiempo de vida de los objetos — adquiere en el constructor, libera en el destructor
  • La Regla de Cinco: si gestionas un recurso, implementa las 5 funciones miembro especiales
  • La Regla de Cero: la mayoria de las clases no deberian declarar ninguna funcion miembro especial — componen a partir de tipos RAII
  • Declarar un destructor suprime la generacion automatica de operaciones de movimiento
  • El idioma copy-and-swap proporciona asignacion segura ante excepciones con minimo codigo

Quiz — Pon a prueba tus conocimientos

(20 XP)

1. Que es RAII?

2. Si una clase define un destructor personalizado, que sucede con sus operaciones de movimiento?

3. Que establece la Regla de Cero?