Estaba viendo las transmisiones de "Going Native 2012" y me di cuenta de la discusión std::shared_ptr
. Me sorprendió un poco escuchar la opinión algo negativa de Bjarne std::shared_ptr
y su comentario de que debería usarse como "último recurso" cuando el tiempo de vida de un objeto es incierto (lo cual creo que, según él, no debería ser el caso).
¿Alguien querría explicar esto con un poco más de profundidad? ¿Cómo podemos programar sin std::shared_ptr
administrar los tiempos de vida de los objetos de una manera segura ?
c++
c++11
smart-pointer
ronag
fuente
fuente
Respuestas:
Si puede evitar la propiedad compartida, su aplicación será más simple y fácil de entender y, por lo tanto, menos susceptible a los errores introducidos durante el mantenimiento. Los modelos de propiedad complejos o poco claros tienden a generar acoplamientos difíciles de seguir de diferentes partes de la aplicación a través de un estado compartido que puede no ser fácilmente rastreable.
Dado esto, es preferible usar objetos con duración de almacenamiento automático y tener subobjetos de "valor". De lo contrario,
unique_ptr
puede ser una buena alternativa parashared_ptr
estar, si no un último recurso, en la lista de herramientas deseables.fuente
El mundo en el que vive Bjarne es muy ... académico, a falta de un término mejor. Si su código puede diseñarse y estructurarse de modo que los objetos tengan jerarquías relacionales muy deliberadas, de modo que las relaciones de propiedad sean rígidas e inflexibles, el código fluye en una dirección (de alto nivel a bajo nivel), y los objetos solo hablan con aquellos de nivel inferior. la jerarquía, entonces no encontrarás mucha necesidad
shared_ptr
. Es algo que usas en esas raras ocasiones en las que alguien tiene que romper las reglas. Pero, de lo contrario, puede pegar todo envector
s u otras estructuras de datos que usen semántica de valores, yunique_ptr
s para cosas que tiene que asignar individualmente.Si bien es un gran mundo para vivir, no es lo que puedes hacer todo el tiempo. Si no puede organizar su código de esa manera, porque el diseño del sistema que está tratando de hacer significa que es imposible (o simplemente profundamente desagradable), entonces encontrará que necesita cada vez más la propiedad compartida de los objetos. .
En dicho sistema, mantener punteros desnudos no es ... exactamente peligroso, pero plantea preguntas. Lo bueno de esto
shared_ptr
es que proporciona garantías sintácticas razonables sobre la vida útil del objeto. ¿Se puede romper? Por supuesto. Pero la gente también puedeconst_cast
cosas; El cuidado básico y la alimentaciónshared_ptr
deben proporcionar una calidad de vida razonable para los objetos asignados cuya propiedad debe ser compartida.Luego, hay
weak_ptr
s, que no se pueden usar en ausencia de ashared_ptr
. Si su sistema está rígidamente estructurado, puede almacenar un puntero desnudo a algún objeto, seguro sabiendo que la estructura de la aplicación asegura que el objeto señalado lo sobrevivirá. Puede llamar a una función que devuelve un puntero a algún valor interno o externo (busque un objeto llamado X, por ejemplo). En un código estructurado adecuadamente, esa función solo estaría disponible si la vida útil del objeto fuera superior a la suya; por lo tanto, almacenar ese puntero desnudo en su objeto está bien.Dado que esa rigidez no siempre es posible de lograr en sistemas reales, necesita alguna forma de garantizar razonablemente la vida útil. A veces, no necesitas la propiedad total; a veces, solo necesita saber cuándo el puntero es malo o bueno. Ahí es donde
weak_ptr
entra. Ha habido casos en los que podría haber usado ununique_ptr
oboost::scoped_ptr
, pero tuve que usar unshared_ptr
porque específicamente necesitaba darle a alguien un puntero "volátil". Un puntero cuya vida fue indeterminada, y podrían preguntar cuándo se destruyó ese puntero.Una forma segura de sobrevivir cuando el estado del mundo es indeterminado.
¿Podría haber sido hecho por alguna llamada de función para obtener el puntero, en lugar de vía
weak_ptr
? Sí, pero eso podría romperse más fácilmente. Una función que devuelve un puntero desnudo no tiene forma de sugerir sintácticamente que el usuario no haga algo como almacenar ese puntero a largo plazo. Devolver unshared_ptr
también hace que sea demasiado fácil para alguien simplemente almacenarlo y potencialmente prolongar la vida útil de un objeto. La devolución de unweak_ptr
embargo fuertemente sugiere que el almacenamiento de lashared_ptr
que se obtiene delock
una ... idea dudosa. No le impedirá hacerlo, pero nada en C ++ le impide romper el código.weak_ptr
proporciona una resistencia mínima al hacer lo natural.Ahora, eso no quiere decir que
shared_ptr
no se pueda usar en exceso ; Ciertamente puede. Especialmente antesunique_ptr
, hubo muchos casos en los que simplemente utilicé unboost::shared_ptr
porque necesitaba pasar un puntero RAII o ponerlo en una lista. Sin movimiento semántico yunique_ptr
,boost::shared_ptr
fue la única solución real.Y puede usarlo en lugares donde es bastante innecesario. Como se indicó anteriormente, la estructura de código adecuada puede eliminar la necesidad de algunos usos de
shared_ptr
. Pero si su sistema no puede estructurarse como tal y sigue haciendo lo que necesita,shared_ptr
será de gran utilidad.fuente
shared_ptr
es ideal para sistemas donde c ++ se integró con lenguaje de script como python. Usandoboost::python
, el recuento de referencias en el lado de c ++ y python coopera enormemente; cualquier objeto de c ++ puede mantenerse en Python y no morirá.shared_ptr
. Ambos usan sus propias implementaciones deintrusive_ptr
. Solo menciono eso porque ambos son ejemplos del mundo real de grandes aplicaciones escritas en C ++shared_ptr
aplica igualmente aintrusive_ptr
: él está objetando todo el concepto de propiedad compartida, no a ninguna ortografía específica del concepto. Entonces, para los propósitos de esta pregunta, esos son dos ejemplos del mundo real de grandes aplicaciones que sí usanshared_ptr
. (Y, lo que es más, demuestran queshared_ptr
es útil incluso cuando no permiteweak_ptr
.)No creo haberlo usado nunca
std::shared_ptr
.La mayoría de las veces, un objeto está asociado con alguna colección, a la que pertenece durante toda su vida útil. En cuyo caso solo puede usar
whatever_collection<o_type>
owhatever_collection<std::unique_ptr<o_type>>
, esa colección es un miembro de un objeto o una variable automática. Por supuesto, si no necesita un número dinámico de objetos, puede usar una matriz automática de tamaño fijo.Ninguna iteración a través de la colección o cualquier otra operación en el objeto requiere una función auxiliar para compartir la propiedad ... usa el objeto, luego regresa, y la persona que llama garantiza que el objeto permanezca vivo durante toda la llamada . Este es, con mucho, el contrato más utilizado entre la persona que llama y la persona que llama.
Nicol Bolas comentó que "si algún objeto se aferra a un puntero desnudo y ese objeto muere ... ¡vaya!". y "Los objetos necesitan asegurarse de que el objeto viva a través de la vida de ese objeto. Solo
shared_ptr
eso puede hacerlo".No compro ese argumento. Al menos eso no
shared_ptr
resuelve este problema. Qué pasa:Al igual que la recolección de basura, el uso predeterminado de
shared_ptr
alienta al programador a no pensar en el contrato entre objetos, o entre la función y la persona que llama. Es necesario pensar en las precondiciones y postcondiciones correctas, y la vida útil de los objetos es solo una pequeña parte de ese pastel más grande.Los objetos no "mueren", un fragmento de código los destruye. Y lanzar
shared_ptr
el problema en lugar de resolver el contrato de llamada es una falsa seguridad.fuente
shared_ptr
yweak_ptr
fueron diseñados para evitar. Bjarne trata de vivir en un mundo donde todo tiene una vida agradable y explícita, y todo se basa en eso. Y si puedes construir ese mundo, genial. Pero no es así en el mundo real. Los objetos necesitan asegurarse de que el objeto viva a través de la vida de ese objeto. Soloshared_ptr
puede hacer eso.shared_ptr
solo mitiga una modificación externa específica, y ni siquiera la más común. Y no es responsabilidad del objeto garantizar que su vida útil sea correcta, si el contrato de llamada de función especifica lo contrario.unique_ptr
, expresando que solo existe un puntero al objeto y que tiene propiedad.shared_ptr
, aún debe devolver aunique_ptr
. La conversión deunique_ptr
ashared_ptr
es fácil, pero lo contrario es lógicamente imposible.Prefiero no pensar en términos absolutos (como "último recurso") sino en relación con el dominio del problema.
C ++ puede ofrecer diferentes formas de administrar la vida útil. Algunos de ellos intentan volver a conducir los objetos de forma apilada. Otros intentan escapar de esta limitación. Algunos de ellos son "literales", otros son aproximaciones.
En realidad puedes:
Person
tienen lo mismoname
son la misma persona (mejor: dos representaciones de una misma persona ). La pila de máquinas concede toda la vida, y el final -esencialmente- no importa para el programa (dado que una persona es su nombre , no importa lo que loPerson
lleve)std::unique_ptr
hace (puede pensarlo como un vector con tamaño 1). Nuevamente, admite que el objeto comienza a existir (y termina su existencia) antes (después) de la estructura de datos a la que se refieren.La debilidad de este método es que los tipos y cantidades de objetos no pueden variar durante la ejecución de llamadas de nivel de pila más profundas con respecto a dónde se crean. Todas estas técnicas "fallan" su fuerza en toda la situación en la que la creación y eliminación de objetos son consecuencia de las actividades del usuario, por lo que el tipo de tiempo de ejecución del objeto no se conoce en tiempo de compilación y puede haber sobreestructuras que se refieren a objetos que el usuario solicita eliminar de una llamada de función de nivel de pila más profunda. En estos casos, debe:
C ++ isteslf no tiene ningún mecanismo nativo para monitorear ese evento (
while(are_they_needed)
), por lo tanto, debe aproximarse con:Yendo a la primera solución a la última, la cantidad de estructura de datos auxiliares requerida para administrar la vida útil del objeto aumenta, a medida que pasa el tiempo para organizarla y mantenerla.
El recolector de basura tiene un costo, shared_ptr tiene menos, unique_ptr aún menos, y los objetos gestionados de pila tienen muy pocos.
¿Es
shared_ptr
el "último recurso"? No, no lo es: el último recurso son los recolectores de basura.shared_ptr
es en realidad elstd::
último recurso propuesto. Pero puede ser la solución correcta, si está en la situación que le expliqué.fuente
La única cosa mencionada por Herb Sutter en una sesión posterior es que cada vez que copia una copia
shared_ptr<>
hay un incremento / decremento entrelazado que tiene que suceder. En el código de subprocesos múltiples en un sistema de múltiples núcleos, la sincronización de memoria no es insignificante. Dada la opción, es mejor usar un valor de pila o aunique_ptr<>
y pasar referencias o punteros sin formato.fuente
shared_ptr
por lvalue o rvalue reference ...shared_ptr
como si fuera la bala de plata que resolverá todos sus problemas de pérdida de memoria solo porque está en el estándar. Es una trampa tentadora, pero sigue siendo importante tener en cuenta la propiedad de los recursos y, a menos que esa propiedad se comparta, ashared_ptr<>
no es la mejor opción.No recuerdo si el último "recurso" fue la palabra exacta que usó, pero creo que el significado real de lo que dijo fue la última "elección": dadas condiciones claras de propiedad; unique_ptr, weak_ptr, shared_ptr e incluso los punteros desnudos tienen su lugar.
Una cosa en la que todos acordaron es que estamos (desarrolladores, autores de libros, etc.) todos en la "fase de aprendizaje" de C ++ 11 y se están definiendo patrones y estilos.
Como ejemplo, Herb explicó que deberíamos esperar nuevas ediciones de algunos de los principales libros de C ++, como Effective C ++ (Meyers) y C ++ Coding Standards (Sutter & Alexandrescu), dentro de un par de años, mientras que la experiencia de la industria y las mejores prácticas con C ++ 11 sartenes.
fuente
Creo que lo que quiere decir es que se está volviendo común que todos escriban shared_ptr cada vez que hayan escrito un puntero estándar (como una especie de reemplazo global), y que se esté utilizando como una copia en lugar de diseñar o al menos planificación para la creación y eliminación de objetos.
La otra cosa que la gente olvida (además del bloqueo / actualización / desbloqueo del cuello de botella mencionado en el material anterior), es que shared_ptr por sí solo no resuelve los problemas del ciclo. Todavía puede filtrar recursos con shared_ptr:
El objeto A, contiene un puntero compartido a otro objeto A El objeto B crea A a1 y A a2, y asigna a1.otherA = a2; y a2.otherA = a1; Ahora, los punteros compartidos del objeto B que usaba para crear a1, a2 quedan fuera de alcance (digamos al final de una función). Ahora tiene una fuga: nadie más se refiere a a1 y a2, pero se refieren entre sí, por lo que sus recuentos de referencias siempre son 1 y usted ha filtrado.
Ese es el ejemplo simple, cuando esto ocurre en código real, generalmente ocurre de manera complicada. Hay una solución con weak_ptr, pero muchas personas ahora solo hacen shared_ptr en todas partes y ni siquiera saben del problema de fuga o incluso de weak_ptr.
Para concluir: creo que los comentarios a los que hace referencia el OP se reducen a esto:
No importa en qué idioma esté trabajando (administrado, no administrado o algo intermedio con recuentos de referencias como shared_ptr), debe comprender y decidir intencionalmente la creación de objetos, vidas y destrucción.
editar: incluso si eso significa "desconocido, necesito usar un shared_ptr", todavía lo has pensado y lo estás haciendo intencionalmente.
fuente
Contestaré desde mi experiencia con Objective-C, un lenguaje donde todos los objetos son contados por referencia y asignados en el montón. Debido a tener una forma de tratar los objetos, las cosas son mucho más fáciles para el programador. Eso ha permitido definir reglas estándar que, cuando se cumplen, garantizan la solidez del código y no hay pérdidas de memoria. También hizo posible que surgieran optimizaciones inteligentes del compilador como el ARC reciente (conteo automático de referencias).
Mi punto es que shared_ptr debería ser tu primera opción en lugar del último recurso. Use el conteo de referencias por defecto y otras opciones solo si está seguro de lo que está haciendo. Serás más productivo y tu código será más robusto.
fuente
Trataré de responder la pregunta:
C ++ tiene una gran cantidad de formas diferentes de hacer memoria, por ejemplo:
struct A { MyStruct s1,s2; };
lugar de shared_ptr en el alcance de la clase. Esto es solo para programadores avanzados porque requiere que comprenda cómo funcionan las dependencias y requiere la capacidad de controlar las dependencias lo suficiente como para restringirlas a un árbol. El orden de las clases en el archivo de encabezado es un aspecto importante de esto. Parece que este uso ya es común con los tipos nativos de c ++ incorporados, pero su uso con clases definidas por el programador parece ser menos utilizado debido a estos problemas de dependencia y orden de clases. Esta solución también tiene problemas con sizeof. Los programadores ven problemas en esto como un requisito para usar declaraciones directas o #incluidos innecesarios y, por lo tanto, muchos programadores recurrirán a una solución inferior de punteros y luego a shared_ptr.MyClass &find_obj(int i);
+ clone () en lugar deshared_ptr<MyClass> create_obj(int i);
. Muchos programadores quieren crear fábricas para crear nuevos objetos. shared_ptr es ideal para este tipo de uso. El problema es que ya asume una solución de administración de memoria compleja que utiliza la asignación de almacenamiento dinámico / libre, en lugar de una solución más simple de pila o basada en objetos. Una buena jerarquía de clases C ++ admite todos los esquemas de administración de memoria, no solo uno de ellos. La solución basada en referencias puede funcionar si el objeto devuelto se almacena dentro del objeto que lo contiene, en lugar de utilizar la variable de alcance de la función local. Se debe evitar pasar la propiedad de la fábrica al código de usuario. Copiar el objeto después de usar find_obj () es una buena forma de manejarlo: los constructores de copia normales y el constructor normal (de diferente clase) con parámetro de referencia o clon () para objetos polimórficos pueden manejarlo.fuente
unique_ptr
es el más adecuado para fábricas. Puede convertir ununique_ptr
ashared_ptr
, pero es lógicamente imposible ir en la otra dirección.