Un std::weak_ptr es un smart pointer que observa sin poseer un objeto gestionado por std::shared_ptr.
Hemos visto std::unique_ptr para cuando queremos ser dueños absolutos. Hemos visto std::shared_ptr para cuando queremos compartir la propiedad entre varias partes del programa.
Pero, ¿qué pasa cuando queremos mirar pero no tocar? O mejor dicho, ¿qué pasa cuando queremos tener una referencia a un objeto gestionado por otros, pero sin impedir que se destruya cuando dejen de usarlo?
Para eso tenemos el tercer tipo de puntero inteligente: el std::weak_ptr.
Este puntero es fundamentalmente un observador débil. A diferencia de su hermano mayor shared_ptr, el weak_ptr no aumenta el contador de referencias (reference count) del objeto.
El problema de las referencias circulares
Para entender la utilidad del weak_ptr, primero tenemos que entender el problema típico de los shared_ptr: las referencias circulares.
Tenemos dos clases, A y B:
Atiene unshared_ptraB.Btiene unshared_ptraA.
struct B; // Forward declaration
struct A {
std::shared_ptr<B> puntero_a_b;
~A() { std::cout << "Adios A\n"; }
};
struct B {
std::shared_ptr<A> puntero_a_a;
~B() { std::cout << "Adios B\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
// Creamos el ciclo
a->puntero_a_b = b;
b->puntero_a_a = a;
}
¿Qué pasa al final del main?
- Se destruye la variable
a(stack). El contador de A baja de 2 a 1 (porquebtodavía apunta a él). - Se destruye la variable
b(stack). El contador de B baja de 2 a 1 (porqueatodavía apunta a él).
Resultado: Ambos objetos se quedan con el contador en 1. Ninguno se destruye. La memoria nunca se libera. Tenemos un Memory Leak.
Romper el ciclo
El criterio práctico en C++ para estas estructuras (como grafos, árboles con referencia al padre, listas doblemente enlazadas) es:
“El dueño tiene un puntero fuerte (
shared_ptr). El subordinado (o la referencia hacia atrás) tiene un puntero débil (weak_ptr).”
Si cambiamos la clase B para usar std::weak_ptr:
struct B {
// CAMBIO IMPORTANTE: weak_ptr en lugar de shared_ptr
std::weak_ptr<A> puntero_a_a;
~B() { std::cout << "Adios B\n"; }
};
Ahora, cuando b apunta a a, no incrementa el contador de referencias de A. Cuando el main termina y se destruyen las variables locales, los contadores llegarán a cero correctamente y todo se limpiará.
¿Cómo usar std::weak_ptr?
Un weak_ptr tiene una particularidad: no permite acceder directamente al objeto.
Como no es dueño del objeto, no podemos estar seguros de que el objeto siga vivo cuando intentemos acceder a él. Podría haber sido borrado por sus dueños (shared_ptr) en otro hilo o en otro momento.
Por eso, weak_ptr no tiene operador * ni operador ->.
El método .lock()
Para usar el objeto al que apunta un weak_ptr, tenemos que “promocionarlo” temporalmente a shared_ptr. Esto se hace con el método .lock().
Esto crea un shared_ptr temporal. Si el objeto existe, el contador sube (evitando que se borre mientras lo usamos). Si el objeto ya murió, nos devuelve nullptr.
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
// INTENTO DE ACCESO 1: El objeto existe
if (std::shared_ptr<int> temp = weak.lock()) {
std::cout << "El valor es: " << *temp << "\n";
} else {
std::cout << "El objeto ya no existe.\n";
}
// "Matamos" al objeto original reseteando el shared_ptr
shared.reset();
// INTENTO DE ACCESO 2: El objeto ya no existe
if (std::shared_ptr<int> temp = weak.lock()) {
std::cout << "El valor es: " << *temp << "\n";
} else {
std::cout << "El objeto ya no existe (correctamente detectado).\n";
}
}
Siempre, siempre usa .lock() y comprueba si el puntero resultante es válido. Es la única forma segura de trabajar con referencias débiles.
Otro método útil: .expired()
Si solo queremos saber si el objeto sigue vivo, pero no necesitamos acceder a él, podemos usar .expired(). Devuelve true si el objeto ya ha sido liberado.
if (weak.expired()) {
std::cout << "Llegamos tarde, el objeto ha muerto.\n";
}
Sin embargo, en entornos concurrentes (multihilo), es más seguro usar .lock(). Entre la llamada a .expired() y el acceso, otro hilo podría destruir el objeto. .lock() intenta obtener una posesión temporal de forma atómica.
El bloque de control
En el artículo anterior vimos que shared_ptr crea un bloque de control en memoria para llevar la cuenta.
En realidad, ese bloque tiene dos contadores:
- Strong Count: Cuántos
shared_ptrhay. Si llega a 0, se destruye el Objeto. - Weak Count: Cuántos
weak_ptrhay. Si llega a 0 (y el Strong Count también es 0), se destruye el Bloque de Control.
Esto significa que, mientras quede un weak_ptr vivo, el bloque de control (unos pocos bytes) seguirá ocupando memoria, aunque el objeto grande que gestionaba ya haya sido liberado. Es un precio pequeño a pagar por la seguridad que nos brinda.
Ahora que sabemos gestionar la memoria, vamos a ver cómo optimizar el movimiento de datos con otro concepto clave: