c++ - ¿Por qué no se puede construir un valor débil de un valor único?
shared-ptr smart-pointers (7)
Conceptualmente, no hay nada que impida una implementación donde un valor débil solo proporciona acceso y un control único controla la vida útil. Sin embargo, hay problemas con eso:
-
unique_ptr
no usa el conteo de referencias para empezar. Agregar la estructura de administración para administrar las referencias débiles sería posible, pero requeriría una asignación dinámica adicional. Dado que se supone queunique_ptr
evita cualquier sobrecarga de tiempo de ejecución (!) Sobre un puntero en bruto, esa sobrecarga no es aceptable. - Para utilizar el objeto al que hace referencia un valor
weak_ptr
, debe extraer una referencia "real" del mismo, lo que primero validará que el puntero no caduca primero y luego le proporciona esta referencia real (unshared_ptr
en este caso). Esto significa que de repente tiene una segunda referencia a un objeto que se supone que es de propiedad única, que es una receta para los errores. Esto no puede solucionarse devolviendo un puntero medio fuerte mixto que solo retrasa temporalmente la posible destrucción del pointee, ya que también podría almacenar ese, también, derrotando la idea detrás deunique_ptr
.
Solo me pregunto, ¿qué problema estás tratando de resolver usando un valor weak_ptr
aquí?
Si comprendo correctamente, weak_ptr
no incrementa el recuento de referencia del objeto gestionado, por lo tanto, no representa la propiedad. Simplemente le permite acceder a un objeto, cuya vida útil es administrada por otra persona. Así que realmente no weak_ptr
por qué un valor weak_ptr
no se puede construir a partir de un unique_ptr
, sino solo un valor shared_ptr
.
¿Alguien puede explicar esto brevemente?
Nadie ha mencionado aún el aspecto de rendimiento del problema, así que permítame agregar mis $ 0.02.
weak_ptr
debe saber de alguna manera cuando todos los shared_ptr
correspondientes han salido de su alcance y el objeto señalado ha sido desasignado y destruido. Esto significa que shared_ptr
s necesita comunicar de alguna manera la destrucción hacia cada weak_ptr
al mismo objeto. Esto tiene un cierto costo, por ejemplo, una tabla hash global necesita actualizarse, donde weak_ptr
obtiene la dirección de (o nullptr
si se destruye el objeto).
Esto también implica bloquear un entorno de subprocesos múltiples, por lo que potencialmente puede ser demasiado lento para algunas tareas.
Sin embargo, el objetivo de unique_ptr
es proporcionar una clase de abstracción estilo RAII de costo cero . Por lo tanto, no debe incurrir en ningún otro costo que el de delete
(o delete[]
ing) el objeto asignado dinámicamente. Sin embargo, la demora impuesta al hacer un acceso a la tabla hash bloqueada o protegida, puede ser comparable al costo de la desasignación, lo cual no es deseable en el caso de unique_ptr
.
Puede ser útil distinguir las razones para preferir un unique_ptr
sobre un shared_ptr
.
Rendimiento Una razón obvia es el uso del tiempo computacional y la memoria. Como se define actualmente, los objetos shared_ptr
normalmente necesitan algo como un valor de recuento de referencia, que ocupa espacio y también debe mantenerse activamente. unique_ptr
objetos unique_ptr
no lo hacen.
Semántica Con un unique_ptr
, usted como programador tiene una idea bastante buena de cuándo se va a destruir el objeto apuntado: cuando se destruye el unique_ptr
o cuando se llama a uno de sus métodos de modificación. Y así, en bases de código grandes o desconocidas, el uso de unique_ptr
transporta de forma estática (y aplica) cierta información sobre el comportamiento en tiempo de ejecución del programa que podría no ser obvia.
Los comentarios anteriores se han centrado generalmente en las razones basadas en el rendimiento por las que sería indeseable que los objetos weak_ptr
se unique_ptr
objetos unique_ptr
. Pero uno podría preguntarse si el argumento basado en la semántica es razón suficiente para que alguna versión futura de la STL respalde el caso de uso implícito en la pregunta original.
Si lo piensa, un valor weak_ptr
debe referirse a algo que no sea el objeto en sí. Esto se debe a que el objeto puede dejar de existir (cuando no hay más punteros a él) y weak_ptr
todavía tiene que referirse a algo que contiene la información de que el objeto ya no existe.
Con shared_ptr
, ese algo es lo que contiene el recuento de referencia. Pero con unique_ptr
, no hay un recuento de referencias, por lo que no hay nada que contenga el recuento de referencias, por lo tanto, no hay nada que siga existiendo cuando el objeto se haya ido. Así que no hay nada que hacer referencia a un valor weak_ptr
.
Tampoco habría una forma weak_ptr
usar un weak_ptr
. Para usarlo, deberías tener alguna forma de garantizar que el objeto no se destruyó mientras lo estabas usando. Eso es fácil con un shared_ptr
- eso es lo que hace un shared_ptr
. ¿Pero cómo haces eso con un unique_ptr
? Obviamente, no puedes tener dos de ellos, y otra cosa ya debe ser propietaria del objeto o se habría destruido ya que tu puntero es débil.
Un shared_ptr
básicamente tiene dos partes:
- el objeto apuntado
- el objeto de recuento de referencia
Una vez que el recuento de referencia cae a cero, el objeto (# 1) se elimina.
Ahora, un valor weak_ptr
debe saber si un objeto aún existe. Para hacer esto, tiene que poder ver el objeto de conteo de referencia (# 2) si no es cero, puede crear un shared_ptr
para el objeto (incrementando el conteo de referencia). Si el recuento es cero, devolverá un shared_ptr
vacío.
¿Considerar ahora cuándo se puede eliminar el objeto de recuento de referencia (# 2)? Debemos esperar hasta que ningún objeto shared_ptr
OR weak_ptr
refiera. Para este propósito, el objeto de conteo de referencia tiene dos conteos de referencia, una referencia fuerte y una referencia débil . El objeto de recuento de referencia solo se eliminará cuando ambos recuentos sean cero. Esto significa que parte de la memoria solo se puede liberar después de que todas las referencias débiles hayan desaparecido (esto implica una desventaja oculta con make_shared
).
tl; dr; weak_ptr
depende de un recuento de referencia débil que forma parte de shared_ptr
, no puede haber un valor weak_ptr
sin un shared_ptr
.
std::weak_ptr
no se puede usar a menos que lo conviertas a std::shared_ptr
por medio de lock()
. si el estándar permitió lo que sugieres, eso significa que necesitas convertir std :: weak_ptr a unique para poder usarlo, violando la unicidad (o reinventar std::shared_ptr
)
Para ilustrar, mira las dos piezas de código:
std::shared_ptr<int> shared = std::make_shared<int>(10);
std::weak_ptr<int> weak(shared);
{
*(w.lock()) = 20; //OK, the temporary shared_ptr will be destroyed but the pointee-integer still has shared to keep it alive
}
Ahora con tu sugerencia:
std::unique_ptr<int> unique = std::make_unique<int>(10);
std::weak_ptr<int> weak(unique);
{
*(weak.lock()) = 20; //not OK. the temporary unique_ptr will be destroyed but unique still points at it!
}
Dicho esto, puede sugerir que solo hay un unique_ptr
, y que aún puede weak_ptr
referencia a weak_ptr
(sin crear otro unique_ptr
), entonces no hay problema. Pero entonces, ¿cuál es la diferencia entre unique_ptr
y shared_ptr
con una referencia? o más, ¿cuál es la diferencia entre un unique_ptr
y punteros C que se obtienen al usar get
?
weak_ptr
no es para "recursos generales que no son propietarios", tiene un trabajo muy específico: el objetivo principal de weak_ptr
es evitar el señalamiento circular de shared_ptr
que shared_ptr
una pérdida de memoria. Cualquier otra cosa es prácticamente necesario hacerlo con plain unique_ptr
y shared_ptr
.
Parece que todos están escribiendo aquí sobre std :: weak_ptr pero no sobre el concepto de poiner débil, que creo que es lo que el autor está pidiendo
Creo que nadie mencionó por qué la biblioteca estándar no proporciona weak_ptr para unique_ptr. El puntero débil CONCEPTO no rechaza el uso de unique_ptr. El puntero débil es solo una información si el objeto ya se ha eliminado, por lo que no es un patrón de observador generalizado mágico sino muy simple.
Es debido a la seguridad de los hilos y la consistencia con shared_ptr.
Simplemente no puede garantizar que su weak_ptr (creado a partir de unique_ptr existente en otro hilo) no se destruya durante el método de llamada en un objeto puntiagudo. Es porque weak_ptr necesita ser consistente con std :: shared_ptr que garantiza la seguridad de los subprocesos. Puede implementar weak_ptr que funciona correctamente con unique_ptr pero solo en el mismo método de bloqueo de subprocesos no será necesario en este caso. Puedes consultar las fuentes de cromo para la base :: WeakPtr y la base :: WeakPtrFactory - puedes usarlo libremente con unique_ptr. El código de puntero débil de Chromium se basa probablemente en la destrucción del último miembro: debe agregar factory como último miembro y, después de eso, creo que WeakPtr está informado sobre la eliminación de objetos (no estoy 100% seguro), por lo que no se ve tan difícil de implementar
En general, el uso de unique_ptr con un concepto de puntero débil está bien, en mi humilde opinión.