Cuando un recurso tiene múltiples dueños y su vida útil no depende de un único objeto, necesitamos un mecanismo de propiedad compartida. std::shared_ptr [C++11] implementa esto mediante un bloque de control (control block) que reside en el heap. Este bloque es una estructura que gestiona dos contadores: el contador fuerte (strong count), que indica cuántos std::shared_ptr son dueños del objeto, y el contador débil (weak count), que rastrea cuántos observadores existen.
El objeto se destruye únicamente cuando el contador fuerte llega a cero, pero el bloque de control solo se libera cuando el contador débil también llega a cero. Esto permite que los observadores sepan si el objeto ha muerto sin impedir su destrucción. Para implementar esto, cada std::shared_ptr ocupa dos punteros: uno hacia el objeto y otro hacia el bloque de control. Además, como los incrementos y decrementos de los contadores deben ser seguros en entornos multihilo, se utilizan operaciones atómicas, lo que introduce un ligero overhead de rendimiento comparado con std::unique_ptr.
Para instanciar estos punteros, lo correcto es usar std::make_shared [C++11] en lugar de new. Al usar std::make_shared, el compilador reserva en una única asignación de memoria tanto el objeto como el bloque de control, mejorando la localidad de datos en la caché y evitando una doble asignación en el heap.
Sin embargo, la propiedad compartida tiene una trampa mortal: las dependencias circulares. Si el Objeto A tiene un std::shared_ptr al Objeto B, y el Objeto B tiene uno al Objeto A, sus contadores fuertes nunca llegarán a cero, provocando una fuga de memoria (memory leak). Aquí es donde entra std::weak_ptr [C++11], un puntero que permite observar un objeto sin poseerlo; no incrementa el contador fuerte, por lo que no impide la destrucción del objeto, pero permite intentar acceder a él de forma segura mediante el método .lock().
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// Necesitamos heredar de esta clase para poder obtener un shared_ptr
// seguro desde una instancia de la propia clase.
class Resource : public std::enable_shared_from_this<Resource> {
public:
explicit Resource(std::string name) : name_(std::move(name)) {
std::cout << "[Resource] " << name_ << " creado\n";
}
~Resource() {
std::cout << "[Resource] " << name_ << " destruido\n";
}
void do_something() const {
std::cout << "[Resource] " << name_ << " está operando\n";
}
// Retorna un shared_ptr que comparte el mismo bloque de control que 'this'
std::shared_ptr<Resource> as_shared() {
return shared_from_this();
}
const std::string& get_name() const { return name_; }
private:
std::string name_;
};
class Observer {
public:
// El observador no es dueño, solo vigila.
void watch(std::weak_ptr<Resource> weak_res) const {
// Para usar el recurso, debemos intentar "promocionarlo" a shared_ptr
if (auto shared_res = weak_res.lock()) {
std::cout << "[Observer] El recurso '" << shared_res->get_name()
<< "' sigue vivo. Accediendo...\n";
shared_res->do_something();
} else {
std::cout << "[Observer] El recurso ha sido destruido. No puedo hacer nada.\n";
}
}
};
int main() {
Observer obs;
std::weak_ptr<Resource> weak_ref;
{
// Uso de make_shared para eficiencia y seguridad
auto res = std::make_shared<Resource>("Caja_Fuerte");
weak_ref = res; // El weak_ptr no incrementa el strong count
std::cout << "--- Dentro del scope principal ---\n";
obs.watch(weak_ref);
{
auto res_alias = res; // Incrementa el strong count
std::cout << "--- Dentro del sub-scope ---\n";
obs.watch(weak_ref);
} // res_alias sale de scope, el contador baja, pero no llega a 0
std::cout << "--- Saliendo del sub-scope ---\n";
} // res sale de scope, strong count llega a 0 -> El Resource se destruye
std::cout << "--- Fuera de todos los scopes ---\n";
// Verificamos si el recurso ha expirado
if (weak_ref.expired()) {
std::cout << "[Main] El recurso ya no existe.\n";
}
obs.watch(weak_ref);
return 0;
}
Desglose del código
Al ejecutar el programa, observa cómo std::make_shared<Resource>("Caja_Fuerte") realiza una única operación de asignación en el heap. El objeto res es un std::shared_ptr que apunta al bloque de control, el cual contiene un contador fuerte con valor 1.
Cuando asignamos weak_ref = res, se incrementa el contador débil en el bloque de control, pero el contador fuerte permanece en 1. Por eso, cuando el objeto res sale de su ámbito en la línea 67, el contador fuerte baja a 0 y el destructor de Resource se llama inmediatamente. Sin embargo, el bloque de control sigue existiendo en memoria porque el weak_ptr todavía lo mantiene vivo (el contador débil es > 0).
En la función watch, utilizamos weak_res.lock(). Este es el paso crítico: lock() intenta incrementar el contador fuerte. Si el objeto ya fue destruido, lock() devuelve un std::shared_ptr vacío (nullptr), lo que nos permite manejar la ausencia del objeto de forma segura sin acceder a memoria invalidada.
La herencia de std::enable_shared_from_this<Resource> es fundamental si necesitaras que Resource pasara un shared_ptr de sí mismo a otra función (usando as_shared()). Sin esto, si intentaras crear un std::shared_ptr<Resource>(this), crearías un segundo bloque de control independiente para el mismo objeto, provocando un desastre de doble liberación al final del programa.
El error frecuente
Un error clásico es intentar crear un shared_ptr desde un puntero this de forma manual:
class MalDiseñado {
public:
std::shared_ptr<MalDiseñado> get_me() {
return std::shared_ptr<MalDiseñado>(this); // ERROR CRÍTICO
}
};
// ... en el main ...
auto ptr1 = std::make_shared<MalDiseñado>();
auto ptr2 = ptr1->get_me(); // ¡Peligro!
En este escenario, ptr1 tiene un bloque de control. ptr2 crea un segundo bloque de control para la misma dirección de memoria. Cuando ambos salgan de ámbito, ambos intentarán liberar el mismo puntero this, provocando un error de corrupción de memoria o un crash inmediato. Si necesitas que un objeto devuelva un shared_ptr de sí mismo, siempre debes usar shared_from_this() y asegurar que el objeto haya sido creado mediante std::make_shared.
N° 62