std::unique_ptr: Propiedad exclusiva y transferencia de recursos

std::unique_ptr [C++11] es un puntero inteligente que implementa la semántica de propiedad exclusiva (exclusive ownership). Esto significa que un objeto reside en la pila (heap) y solo un objeto std::unique_ptr puede ser el dueño de esa dirección de memoria en un momento dado. Internamente, es un wrapper sobre un puntero crudo que utiliza el principio RAII (Resource Acquisition Is Initialization) para garantizar que, en cuanto el unique_ptr sale de su ámbito (scope), el objeto al que apunta sea destruido automáticamente.

Para lograr esta exclusividad, la clase tiene prohibido su uso en operaciones de copia; es un tipo de sólo movimiento (move-only type). Esto evita que dos punteros intenten liberar la misma memoria, lo cual causaría un error de doble liberación (double-free). Sin embargo, puede transferir su propiedad a otro objeto mediante std::move. Para instanciarlo, la práctica estándar es usar std::make_unique [C++14], que es más seguro contra fugas de memoria en expresiones complejas y más limpio que usar new.

Debes usar std::unique_ptr siempre que un objeto tenga un ciclo de vida único y bien definido, como un componente de un sistema que solo pertenece a un contenedor. Si intentas copiarlo, el compilador te detendrá. Si usas release() de forma incorrecta, estarás renunciando a la gestión automática y serás responsable de la liberación manual, lo que rompe la seguridad del modelo; y si usas get() para almacenar el puntero crudo y el unique_ptr original muere, te quedas con un puntero colgante (dangling pointer).

#include <iostream>
#include <memory>
#include <utility>
#include <string>
#include <vector>
#include <cstdio>

// Un recurso que requiere una limpieza especial (ej. un archivo)
struct FileDeleter {
    void operator()(FILE* fp) const {
        if (fp) {
            std::cout << "Cerrando archivo mediante custom deleter...\n";
            std::fclose(fp);
        }
    }
};

class Sensor {
public:
    explicit Sensor(std::string nombre) : nombre_(std::move(nombre)) {
        std::cout << "Sensor " << nombre_ << " inicializado.\n";
    }
    ~Sensor() { std::cout << "Sensor " << nombre_ << " destruido.\n"; }

    // Evitamos que el sensor se copie accidentalmente
    Sensor(const Sensor&) = delete;
    Sensor& operator=(const Sensor&) = delete;

    void leer() const { std::cout << "Leyendo datos de " << nombre_ << "...\n"; }

private:
    std::string nombre_;
};

// Recibe la propiedad: el sensor se destruirá al salir de esta función
void procesar_sensor_y_eliminar(std::unique_ptr<Sensor> s) {
    if (s) {
        s->leer();
    }
    std::cout << "Saliendo de procesar_sensor...\n";
}

// Solo accede: no le interesa la gestión de la vida del objeto
void observar_sensor(Sensor* s) {
    if (s) {
        std::cout << "Observando sensor: ";
        s->leer();
    }
}

int main() {
    // 1. Uso de std::make_unique [C++14]: La forma preferida
    auto sensor_principal = std::make_unique<Sensor>("Termómetro");

    // 2. Acceso mediante .get() para pasar a funciones que no gestionan memoria
    observar_sensor(sensor_principal.get());

    // 3. Transferencia de propiedad mediante std::move
    // sensor_principal queda como nullptr después de esto
    procesar_sensor_y_eliminar(std::move(sensor_principal));

    // 4. Uso de reset() para cambiar el recurso o liberar el actual
    auto sensor_secundario = std::make_unique<Sensor>("Presión");
    sensor_secundario.reset(new Sensor("Humedad")); // Libera "Presión" y toma "Humedad"
    sensor_secundario.reset(); // Libera "Humedad", sensor_secundario es ahora nullptr

    // 5. Especialización para arrays (usa delete[] automáticamente)
    auto buffer = std::make_unique<int[]>(5);
    buffer[0] = 42;

    // 6. Custom Deleter: Gestión de recursos no estándar (C-style)
    std::unique_ptr<FILE, FileDeleter> archivo(std::fopen("test.txt", "w"));
    if (archivo) {
        std::fputs("Hello C++", archivo.get());
    }

    return 0;
}

Desglose del ejemplo

En el código, std::make_unique<Sensor> reserva memoria en el heap y construye el objeto Sensor directamente, evitando el uso explícito de new. Cuando llamamos a procesar_sensor_y_eliminar(std::move(sensor_principal)), el operador std::move realiza un cast a un lvalue de categoría rvalue, permitiendo que el constructor de movimiento de std::unique_ptr transfiera la dirección de memoria al parámetro s. En ese instante, sensor_principal en el main queda en un estado válido pero nulo.

Cuando la función procesar_sensor_y_eliminar termina, el parámetro s sale de su ámbito y su destructor llama a delete sobre el puntero, liberando el objeto. Observa que observar_sensor recibe un puntero crudo mediante sensor_principal.get(). Esto es correcto para acceso porque la función no asume la propiedad del objeto; es una política de “observación” y no de “posesión”.

Para el caso del archivo, hemos definido un FileDeleter. A diferencia del caso estándar donde se usa delete, aquí el unique_ptr llama a std::fclose. Esto es vital para recursos que no se gestionan con delete/new, como handles de Windows o descriptores de archivos en sistemas POSIX. Finalmente, la especialización std::unique_ptr<int[]> cambia el comportamiento interno del destructor para invocar delete[] en lugar de delete, evitando errores de desasignación de memoria en arreglos.

El error frecuente

Un error clásico es confundir release() con reset().

// ERROR: Fuga de memoria (Memory Leak)
std::unique_ptr<Sensor> s = std::make_unique<Sensor>("Error");
s.release(); // El puntero se suelta, pero nadie lo captura para liberarlo.

Al llamar a s.release(), el unique_ptr renuncia a su propiedad y devuelve el puntero crudo, pero no destruye el objeto. Si no guardas ese puntero en otra variable que gestione la vida del objeto (como otro unique_ptr o un puntero crudo que luego pases a un delete), habrás creado una fuga de memoria. reset(), en cambio, destruye el objeto actual antes de (opcionalmente) tomar uno nuevo, manteniendo siempre la seguridad de RAII.

Para compilar este ejemplo:
g++ -std=c++20 -Wall -Wextra -Wpedantic -o example example.cpp

61

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio