c++ - ¿Cómo funciona weak_ptr?
boost weak-references (2)
Básicamente, un "weak_ptr" es un puntero "T *" común que le permite RECUPERAR una referencia fuerte, es decir, "shared_ptr", más adelante en el código.
Al igual que un T * ordinario, el weak_ptr no hace ningún recuento de referencia. Internamente, para admitir el recuento de referencias en un tipo arbitrario T, el STL (o cualquier otra biblioteca que implemente este tipo de lógica) crea un objeto contenedor que llamaremos "Ancla". El "anclaje" existe únicamente para implementar el recuento de referencias y el comportamiento "cuando el recuento es cero, llamar y eliminar" que necesitamos.
En una referencia sólida, shared_ptr implementa su copia, operator =, constructor, destructor y otras API pertinentes para actualizar el recuento de referencias de "Anchor". Así es como shared_ptr asegura que tu "T" viva exactamente mientras alguien la esté usando. En un "weak_ptr", esas mismas API solo copian el ptr de ancla real. NO actualizan recuentos de referencia.
Esta es la razón por la que las API más importantes de "weak_ptr" están "expiradas" y el "bloqueo" mal llamado. "Caducado" le dice si el objeto subyacente aún está presente, es decir, "¿Ya se borró a sí mismo porque todas las referencias sólidas quedaron fuera del alcance?". "Bloquear" convertirá (si puede) el weak_ptr a una referencia fuerte shared_ptr, restaurando el conteo de referencia.
Por cierto, "bloqueo" es un terrible nombre para esa API. No estás (simplemente) invocando un mutex, estás creando una referencia fuerte desde una débil, con ese "Ancla" actuando. El mayor defecto en ambas plantillas es que no implementaron operator->, por lo que para hacer cualquier cosa con su objeto, debe recuperar la "T *" en bruto. Lo hicieron principalmente para soportar cosas como "shared_ptr", porque los tipos primitivos no son compatibles con el operador "->".
Entiendo cómo usar weak_ptr
y shared_ptr
. Entiendo cómo funciona shared_ptr
, contando el número de referencias en su objeto. ¿Cómo funciona weak_ptr
? Intenté leer el código fuente de refuerzo, y no estoy lo suficientemente familiarizado con el impulso para comprender todas las cosas que usa.
Gracias.
shared_ptr
usa un objeto extra "contador" (también conocido como "conteo compartido" o "bloque de control") para almacenar el conteo de referencia. (Por cierto: ese objeto "contador" también almacena el eliminador).
Cada shared_ptr
y weak_ptr
contiene un puntero a la punta real, y un segundo puntero al objeto "counter".
Para implementar weak_ptr
, el objeto "contador" almacena dos contadores diferentes:
- El "recuento de uso" es la cantidad de instancias
shared_ptr
apuntan al objeto. - El "conteo débil" es el número de instancias de
weak_ptr
apuntan al objeto, más uno si el "recuento de uso" es aún> 0.
La punta se borra cuando el "recuento de uso" llega a cero.
El objeto auxiliar "contador" se elimina cuando el "conteo débil" llega a cero (lo que significa que el "conteo de uso" también debe ser cero, ver arriba).
Cuando intenta obtener un shared_ptr
de un weak_ptr
, la biblioteca verifica atómicamente el "uso count", y si es> 0 lo incrementa. Si eso tiene éxito, obtienes tu shared_ptr
. Si el "recuento de uso" ya era cero, en su lugar se obtiene una instancia de shared_ptr
vacía.
EDITAR : Ahora, ¿por qué agregan uno al conteo débil en lugar de simplemente liberar el objeto "contador" cuando ambos conteos bajan a cero? Buena pregunta.
La alternativa sería eliminar el objeto "contador" cuando tanto el "conteo de uso" como el "conteo débil" caigan a cero. Esta es la primera razón: no es posible verificar dos contadores (del tamaño de un puntero) de manera atómica en todas las plataformas, e incluso donde sea, es más complicado que consultar solo un contador.
Otra razón es que el eliminador debe permanecer válido hasta que haya terminado de ejecutarse. Como el eliminador se almacena en el objeto "contador", eso significa que el objeto "contador" debe permanecer válido. Considere lo que podría pasar si hay un shared_ptr
y un weak_ptr
para algún objeto, y se restablecen al mismo tiempo en hilos concurrentes. Digamos que shared_ptr
es lo primero. Disminuye el "conteo de uso" a cero, y comienza a ejecutar el eliminador. Ahora el weak_ptr
disminuye el "conteo débil" a cero, y encuentra que el "conteo de uso" es cero también. Por lo tanto, elimina el objeto "contador" y con él el eliminador. Mientras el eliminador aún se está ejecutando.
Por supuesto, habría diferentes maneras de asegurar que el objeto "contador" permanezca vivo, pero creo que incrementar el "conteo débil" por uno es una solución muy elegante e intuitiva. El "conteo débil" se convierte en el recuento de referencia para el objeto "contador". Y como shared_ptr
s también hace referencia al objeto contador, ellos también tienen que incrementar el "conteo débil".
Una solución probablemente más intuitiva sería incrementar el "recuento débil" para cada shared_ptr
, ya que cada shared_ptr
es una referencia al objeto "counter".
Agregar una para todas shared_ptr
instancias shared_ptr
es solo una optimización (guarda un incremento / decremento atómico al copiar / asignar instancias de shared_ptr
).