¿Cuándo es útil std::weak_ptr?
Comencé a estudiar punteros inteligentes de C++ 11 y no veo ningún uso útil std::weak_ptr
. ¿Alguien puede decirme cuándo std::weak_ptr
es útil/necesario?
std::weak_ptr
es una muy buena manera de resolver el problema del puntero colgante . Con solo usar punteros sin formato, es imposible saber si los datos a los que se hace referencia se han desasignado o no. En cambio, al permitir que un std::shared_ptr
administrador administre los datos y los proporcione std::weak_ptr
a los usuarios, los usuarios pueden verificar la validez de los datos llamando expired()
a o lock()
.
No podría hacer esto std::shared_ptr
solo, porque todas std::shared_ptr
las instancias comparten la propiedad de los datos que no se eliminan antes de que std::shared_ptr
se eliminen todas las instancias. Aquí hay un ejemplo de cómo verificar si hay un puntero colgando usando lock()
:
#include <iostream>
#include <memory>
int main()
{
// OLD, problem with dangling pointer
// PROBLEM: ref will point to undefined data!
int* ptr = new int(10);
int* ref = ptr;
delete ptr;
// NEW
// SOLUTION: check expired() or lock() to determine if pointer is valid
// empty definition
std::shared_ptr<int> sptr;
// takes ownership of pointer
sptr.reset(new int);
*sptr = 10;
// get pointer to data without taking ownership
std::weak_ptr<int> weak1 = sptr;
// deletes managed object, acquires new pointer
sptr.reset(new int);
*sptr = 5;
// get pointer to new data without taking ownership
std::weak_ptr<int> weak2 = sptr;
// weak1 is expired!
if(auto tmp = weak1.lock())
std::cout << "weak1 value is " << *tmp << '\n';
else
std::cout << "weak1 is expired\n";
// weak2 points to new data (5)
if(auto tmp = weak2.lock())
std::cout << "weak2 value is " << *tmp << '\n';
else
std::cout << "weak2 is expired\n";
}
Producción
weak1 is expired
weak2 value is 5
Un buen ejemplo sería un caché.
Para los objetos a los que se accedió recientemente, desea mantenerlos en la memoria, por lo que mantiene un puntero fuerte hacia ellos. Periódicamente, escanea el caché y decide a qué objetos no se ha accedido recientemente. No es necesario que los guardes en la memoria, por lo que te deshaces del puntero fuerte.
Pero, ¿qué pasa si ese objeto está en uso y algún otro código tiene un fuerte puntero hacia él? Si el caché se deshace de su único puntero al objeto, nunca podrá volver a encontrarlo. Por lo tanto, el caché mantiene un puntero débil a los objetos que necesita encontrar si permanecen en la memoria.
Esto es exactamente lo que hace un puntero débil: le permite localizar un objeto si todavía está presente, pero no lo mantiene si nada más lo necesita.
Otra respuesta, con suerte más sencilla. (para compañeros de Google)
Supongamos que tiene objetos Team
y Member
.
Obviamente es una relación: el Team
objeto tendrá punteros a su Members
. Y es probable que los miembros también tengan un puntero hacia atrás a su Team
objeto.
Entonces tienes un ciclo de dependencia. Si usa shared_ptr
, los objetos ya no se liberarán automáticamente cuando abandone la referencia a ellos, porque se hacen referencia entre sí de forma cíclica. Esta es una pérdida de memoria.
Rompes esto usando weak_ptr
. El "propietario" normalmente usa shared_ptr
y el "propiedad" usa a weak_ptr
para su padre, y lo convierte temporalmente cuandoshared_ptr
necesita acceso a su padre.
Almacenar un ptr débil:
weak_ptr<Parent> parentWeakPtr_ = parentSharedPtr; // automatic conversion to weak from shared
luego úsalo cuando sea necesario
shared_ptr<Parent> tempParentSharedPtr = parentWeakPtr_.lock(); // on the stack, from the weak ptr
if( !tempParentSharedPtr ) {
// yes, it may fail if the parent was freed since we stored weak_ptr
} else {
// do stuff
}
// tempParentSharedPtr is released when it goes out of scope
Aquí hay un ejemplo, que me dio @jleahy: suponga que tiene una colección de tareas, ejecutadas de forma asincrónica y administradas por un archivo std::shared_ptr<Task>
. Es posible que desee hacer algo con esas tareas periódicamente, por lo que un evento de temporizador puede atravesar std::vector<std::weak_ptr<Task>>
y dar a las tareas algo que hacer. Sin embargo, simultáneamente una tarea puede haber decidido que ya no es necesaria y morir. De este modo, el temporizador puede comprobar si la tarea sigue activa creando un puntero compartido a partir del puntero débil y utilizando ese puntero compartido, siempre que no sea nulo.