cpp-unique-ptr

Qué es y cómo usar std::unique_ptr en C++

  • 4 min

Un std::unique_ptr es un smart pointer con propiedad exclusiva sobre el objeto al que apunta.

Es el puntero inteligente que deberías usar por defecto cuando necesites memoria dinámica. Es ligero, rápido y semánticamente muy claro: representa la propiedad exclusiva de un recurso.

¿Qué es std::unique_ptr?

std::unique_ptr es un wrapper que asegura que solo existe un único propietario para el objeto al que apunta.

Imagina que tienes una llave de una caja fuerte.

  • Solo tú tienes la llave.
  • Si quieres que otra persona tenga acceso, tienes que darle tu llave.
  • Tú te quedas sin ella. No puedes duplicarla.

Esto tiene implicaciones inmediatas en el código:

  1. No se puede copiar: Si intentas copiar un unique_ptr, el compilador te dará un error.
  2. Se puede mover: Puedes transferir la propiedad a otro unique_ptr, pero el original quedará vacío (nullptr).
  3. Destrucción automática: Cuando el unique_ptr sale del ámbito, destruye el objeto que posee.

Con el eliminador predeterminado, std::unique_ptr suele tener el mismo tamaño que un puntero crudo y no necesita contadores de referencias ni bloqueos. Un eliminador personalizado con estado sí puede aumentar su tamaño.

Creación de un unique_ptr

Al igual que con los punteros compartidos, tenemos una función auxiliar para crearlos: std::make_unique. Esta función llegó en C++14 (un poco más tarde que C++11), pero es el estándar hoy en día.

#include <memory>
#include <iostream>

int main() {
    // FORMA CORRECTA (C++14 en adelante)
    std::unique_ptr<int> numero = std::make_unique<int>(42);

    // Acceso al valor (igual que un puntero normal)
    std::cout << "Valor: " << *numero << "\n";
    
    // Podemos resetearlo manualmente si queremos (borra la memoria)
    numero.reset(); 
    
    if (numero == nullptr) {
        std::cout << "El puntero esta vacio\n";
    }
}
Copied!

Propiedad exclusiva: copia frente a movimiento

Este es el punto donde mucha gente se confunde al principio. Vamos a verlo con código.

Intento de copia

std::unique_ptr<int> p1 = std::make_unique<int>(10);

// ¡ERROR DE COMPILACIÓN!
// std::unique_ptr<int> p2 = p1; 
Copied!

El compilador prohíbe esto porque, si permitiera dos punteros únicos al mismo sitio, ambos intentarían borrar la memoria al salir. Resultado: double free y crash del programa.

Transferencia de propiedad

Si queremos que p2 gestione el recurso, tenemos que moverlo explícitamente usando std::move.

std::unique_ptr<int> p1 = std::make_unique<int>(10);

// CORRECTO: Transferimos la propiedad
std::unique_ptr<int> p2 = std::move(p1);

if (!p1) {
    std::cout << "p1 ahora es nullptr (ha perdido la propiedad)\n";
}

std::cout << "p2 es el nuevo dueño: " << *p2 << "\n";
Copied!

Tras el move, p1 queda en un estado válido pero vacío (apunta a nullptr). p2 es ahora el único responsable de borrar la memoria.

Paso a funciones: ¿Por valor o referencia?

Esta es una duda muy común: “¿Cómo paso un unique_ptr a una función?”. La respuesta depende de qué quieres hacer con la propiedad.

1. Solo quiero usar el objeto (Observar)

Si la función solo necesita leer o modificar el objeto, pero no gestionarlo, pasa una referencia al objeto o un puntero crudo (usando .get()). No muevas el unique_ptr.

// Opción A: Referencia const (Recomendada para solo lectura)
void imprimir(const MiClase& obj) {
    obj.mostrar();
}

// Opción B: Puntero crudo (Si puede ser nullptr)
void procesar(MiClase* obj) {
    if(obj) obj->calcular();
}

int main() {
    auto ptr = std::make_unique<MiClase>();
    
    imprimir(*ptr);      // Pasamos el objeto, ptr sigue siendo dueño
    procesar(ptr.get()); // Pasamos puntero crudo, ptr sigue siendo dueño
}
Copied!

2. Quiero transferir la responsabilidad (Transferir)

Si la función va a quedarse con el objeto (por ejemplo, añadirlo a una lista interna), entonces pasas el unique_ptr por valor y usas std::move al llamar.

void guardarEnBaseDeDatos(std::unique_ptr<MiClase> obj) {
    // Ahora 'obj' es propiedad de esta función.
    // Se destruirá cuando termine la función (o se moverá a otro lado).
}

int main() {
    auto ptr = std::make_unique<MiClase>();
    
    guardarEnBaseDeDatos(std::move(ptr));
    
    // AQUÍ ptr YA NO VALE NADA. No lo uses.
}
Copied!

Uso en colecciones

Gracias a la semántica de movimiento, los unique_ptr son perfectos para guardarlos en std::vector u otros contenedores. Esto era muy difícil en C++98, pero en C++ Moderno es trivial.

std::vector<std::unique_ptr<Enemigo>> enemigos;

enemigos.push_back(std::make_unique<Enemigo>("Nivel 1"));
enemigos.push_back(std::make_unique<Enemigo>("Nivel 2"));

// Al limpiarse el vector, se limpian todos los enemigos automáticamente
// enemigos.clear(); // Destruye todos los objetos
Copied!

Usar std::vector<std::unique_ptr<T>> es la forma estándar de manejar colecciones polimórficas (una lista de objetos de distintas clases hijas) en C++.