cpp-weak-ptr

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

  • 5 min

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:

  • A tiene un shared_ptr a B.
  • B tiene un shared_ptr a A.
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;
}
Copied!

¿Qué pasa al final del main?

  1. Se destruye la variable a (stack). El contador de A baja de 2 a 1 (porque b todavía apunta a él).
  2. Se destruye la variable b (stack). El contador de B baja de 2 a 1 (porque a todaví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"; }
};
Copied!

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";
    }
}
Copied!

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";
}
Copied!

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:

  1. Strong Count: Cuántos shared_ptr hay. Si llega a 0, se destruye el Objeto.
  2. Weak Count: Cuántos weak_ptr hay. 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.