Ir al contenido

shared_ptr, weak_ptr y Grafos de Propiedad

Comprende la propiedad compartida con conteo de referencias con shared_ptr, rompe ciclos con weak_ptr, aprende sobre bloques de control y aliasing, y sabe cuando shared_ptr es la eleccion incorrecta.

shared_ptr: Propiedad Compartida

std::shared_ptr habilita la propiedad compartida — multiples instancias de shared_ptr pueden poseer el mismo objeto. El objeto se destruye cuando el ultimo shared_ptr propietario es destruido o reseteado.

Internamente, shared_ptr mantiene un bloque de control que contiene:
- Un conteo de referencias fuertes (numero de propietarios shared_ptr)
- Un conteo de referencias debiles (numero de observadores weak_ptr)
- El deleter y el asignador

shared_ptr es mas grande que un puntero crudo (tipicamente 2 punteros: uno al objeto, uno al bloque de control) y el conteo de referencias tiene sobrecarga atomica en cada copia/destruccion. Usalo solo cuando la propiedad sea genuinamente compartida.

make_shared y Conteo de Referencias

std::make_shared(args...) asigna el objeto y el bloque de control en una unica asignacion — mejor localidad de cache, menos fragmentacion de memoria y seguridad ante excepciones.

shared_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Logger {
    std::string name;
    Logger(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }
    ~Logger() { std::cout << name << " destroyed\n"; }
    void log(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << '\n';
    }
};

int main() {
    auto logger = std::make_shared<Logger>("AppLogger");
    std::cout << "use_count: " << logger.use_count() << '\n';  // 1

    {
        auto copy = logger;  // shared ownership
        std::cout << "use_count: " << logger.use_count() << '\n';  // 2
        copy->log("inside scope");
    }  // copy destroyed, count drops to 1

    std::cout << "use_count: " << logger.use_count() << '\n';  // 1
    logger->log("still alive");
}  // logger destroyed — count drops to 0, Logger deleted

weak_ptr: Rompiendo Ciclos

std::weak_ptr es un observador sin propiedad de un objeto gestionado por shared_ptr. No incrementa el conteo de referencias fuertes — asi que no mantiene vivo el objeto.

Caso de uso principal: romper ciclos de propiedad. Si A posee un shared_ptr a B y B posee un shared_ptr a A, ninguno sera destruido jamas (conteo de referencias circular). Haz que uno de los punteros sea un weak_ptr para romper el ciclo.

Para acceder al objeto, llama a weak_ptr::lock(), que retorna un shared_ptr. Si el objeto ya fue destruido, lock() retorna un shared_ptr nulo. Siempre verifica el resultado de lock().

Ciclo de weak_ptr en Accion

Este ejemplo muestra como weak_ptr rompe un ciclo de propiedad que de otro modo evitaria la destruccion.

El Constructor Aliasing

El constructor aliasing de shared_ptr te permite crear un shared_ptr que comparte propiedad con un shared_ptr existente pero apunta a un subobjeto diferente. El objeto original permanece vivo mientras cualquier shared_ptr aliased exista.

struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(1, 2);
std::shared_ptr<int> q(p, &p->first);  // shares ownership, points to first

Esto es util para pasar un miembro de un objeto gestionado por shared_ptr a codigo que espera un shared_ptr. El conteo de referencias del objeto padre se usa, asi que el padre vive mientras alguien tenga un puntero aliased a cualquier parte de el.

Error común

1. Ciclos de propiedad — dos shared_ptr apuntandose mutuamente nunca liberan memoria. Usa weak_ptr para romper ciclos.

2. Construir desde this — shared_ptr(this) dentro de un metodo crea un bloque de control separado, causando doble delete. Usa std::enable_shared_from_this en su lugar.

3. Sobre-compartir — usar shared_ptr en todas partes "por si acaso" es un anti-patron. Paga la sobrecarga atomica solo cuando la propiedad compartida es genuinamente necesaria. Por defecto usa unique_ptr.

4. No es seguro para hilos — el conteo de referencias es atomico, pero el objeto apuntado no. Multiples hilos leyendo/escribiendo el mismo shared_ptr (no el objeto, el propio puntero) requiere sincronizacion o std::atomic> (C++20).

enable_shared_from_this

Cuando un objeto gestionado por shared_ptr necesita obtener un shared_ptr a si mismo (por ejemplo, para registrarse en un callback), debe heredar de std::enable_shared_from_this y llamar a shared_from_this().

control_block.cpp
#include <memory>
#include <iostream>

struct Widget {
    int data[4];
};

int main() {
    // Two allocations: one for Widget, one for control block
    std::shared_ptr<Widget> p1(new Widget{});

    // Single allocation: Widget + control block together
    auto p2 = std::make_shared<Widget>();

    // Sizes (typical 64-bit platform)
    std::cout << "sizeof(Widget*):          " << sizeof(Widget*) << '\n';   // 8
    std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n';  // 8
    std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n';  // 16

    // shared_ptr is 2 pointers: one to object, one to control block
    // Control block contains: strong count, weak count, deleter, allocator

    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // 1
    std::weak_ptr<Widget> w = p2;
    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // still 1
    // weak_ptr does NOT increase strong count
}
Puntos clave
  • shared_ptr proporciona propiedad compartida con conteo de referencias — el objeto se destruye cuando el ultimo propietario muere
  • make_shared asigna el objeto y el bloque de control juntos — una asignacion en vez de dos
  • weak_ptr rompe ciclos de propiedad — observa sin mantener vivo el objeto
  • Siempre verifica weak_ptr::lock() — el objeto puede haber sido destruido
  • Usa enable_shared_from_this cuando un objeto necesita un shared_ptr a si mismo
  • Por defecto usa unique_ptr — shared_ptr solo cuando la propiedad compartida es genuinamente necesaria

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Que sucede cuando dos objetos mantienen `shared_ptr` el uno al otro?

2. Cual es la ventaja de `make_shared` sobre `shared_ptr<T>(new T(...))`?

3. Como accedes de forma segura a un objeto a traves de un `weak_ptr`?