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:
- No se puede copiar: Si intentas copiar un
unique_ptr, el compilador te dará un error. - Se puede mover: Puedes transferir la propiedad a otro
unique_ptr, pero el original quedará vacío (nullptr). - Destrucción automática: Cuando el
unique_ptrsale 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";
}
}
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;
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";
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
}
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.
}
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
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++.