std :: shared_ptr como último recurso?

58

Estaba viendo las transmisiones de "Going Native 2012" y noté la discusión sobre std::shared_ptr . Me sorprendió un poco escuchar la opinión algo negativa de Bjarne sobre 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 que creo que, según él, debería ser poco frecuente) caso).

¿A alguien le importaría explicar esto con un poco más de profundidad? ¿Cómo podemos programar sin std::shared_ptr y seguir administrando la vida útil de los objetos de forma segura ?

    
pregunta ronag 04.02.2012 - 15:47

9 respuestas

56

Si puede evitar la propiedad compartida, su aplicación será más sencilla 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 conducir a vínculos 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 utilizar objetos con duración de almacenamiento automático y tener subobjetos de "valor". Si esto falla, unique_ptr puede ser una buena alternativa, ya que shared_ptr es, si no es el último recurso, un poco más abajo en la lista de herramientas deseables.

    
respondido por el CB Bailey 04.02.2012 - 16:00
47

El mundo en el que vive Bjarne es muy ... académico, por falta de un término mejor. Si su código puede diseñarse y estructurarse de manera tal que los objetos tengan jerarquías relacionales muy deliberadas, de manera que las relaciones de propiedad sean rígidas e inflexibles, el código fluye en una dirección (desde un nivel alto hasta un nivel bajo) y los objetos solo hablan con los que están más abajo. la jerarquía, entonces no encontrará mucha necesidad de shared_ptr . Es algo que se usa en esas raras ocasiones en que alguien tiene que romper las reglas. Pero de lo contrario, puede pegar todo en vector s u otras estructuras de datos que utilizan valores semánticos, y unique_ptr s para las cosas que tiene que asignar por separado.

Si bien es un gran mundo para vivir, no es lo que haces todo el tiempo. Si no puede organizar su código de esa manera, ya que el diseño del sistema que está tratando de hacer significa que es imposible (o simplemente muy desagradable), entonces encontrará que necesita propiedad compartida de objetos cada vez más.

En un sistema de este tipo, mantener los punteros desnudos no es ... exactamente peligroso, pero sí plantea preguntas. Lo mejor de 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 puede const_cast cosas; la atención básica y la alimentación de shared_ptr deben proporcionar una calidad de vida razonable para los objetos asignados, cuya propiedad se debe compartir.

Luego, hay weak_ptr s, que no se puede usar en ausencia de un shared_ptr . Si su sistema está rígidamente estructurado, entonces puede almacenar un puntero desnudo en algún objeto, con la certeza de que la estructura de la aplicación garantiza que el objeto al que apunta lo sobrevivirá. Puede llamar a una función que devuelve un puntero a algún valor interno o externo (por ejemplo, busque un objeto llamado X). En un código estructurado correctamente, 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 no siempre es posible lograr la rigidez en sistemas reales, necesita alguna forma de garantizar razonablemente la vida útil. A veces, no necesitas la propiedad completa; a veces, solo necesitas saber cuándo el puntero es bueno o malo. Ahí es donde entra weak_ptr . Ha habido casos en los que podría haber usado unique_ptr o boost::scoped_ptr , pero tuve que usar shared_ptr porque específicamente necesitaba dar a alguien un puntero "volátil". Un puntero cuya vida útil era indeterminada, y podían consultar cuándo se destruyó ese puntero.

Una forma segura de sobrevivir cuando el estado del mundo es indeterminado.

¿Se podría haber hecho con alguna llamada de función para obtener el puntero, en lugar de a través de weak_ptr ? Sí, pero eso podría romperse más fácilmente. Una función que devuelve un puntero desnudo no tiene manera de sugerir de manera sintáctica que el usuario no haga algo como almacenar ese puntero a largo plazo. Devolver un shared_ptr también hace que sea demasiado fácil para alguien simplemente almacenarlo y potencialmente prolongar la vida útil de un objeto. Sin embargo, devolver un weak_ptr sugiere fuertemente que almacenar el shared_ptr que obtiene de lock es una ... idea dudosa. No le impedirá que lo haga, pero nada en C ++ le impide romper el código. weak_ptr proporciona cierta resistencia mínima al hacer lo natural.

Ahora, eso no quiere decir que shared_ptr no pueda ser usado en exceso ; ciertamente puede Especialmente antes de unique_ptr , hubo muchos casos en los que simplemente usé boost::shared_ptr porque necesitaba pasar un puntero RAII o ponerlo en una lista. Sin mover la semántica y unique_ptr , boost::shared_ptr fue la única solución real.

Y puedes usarlo en lugares donde no sea necesario. 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 se puede estructurar como tal y aún hace lo que necesita, shared_ptr será de gran utilidad.

    
respondido por el Nicol Bolas 04.02.2012 - 17:11
37

No creo que haya usado 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. En cuyo caso, simplemente puede usar whatever_collection<o_type> o whatever_collection<std::unique_ptr<o_type>> , ya que 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 permanece vivo durante toda la llamada . Este es, con mucho, el contrato más utilizado entre el llamante y el que recibe la llamada.

Nicol Bolas comentó que "si algún objeto se mantiene sobre un puntero desnudo y ese objeto muere ... oops". y "Los objetos deben asegurarse de que el objeto viva a lo largo de la vida de ese objeto. Solo shared_ptr puede hacer eso".

No compro ese argumento. Al menos no que shared_ptr resuelva este problema. ¿Qué pasa con:

  • Si alguna tabla hash se mantiene en un objeto y el código hash de ese objeto cambia ... oops.
  • Si alguna función está iterando un vector y un elemento se inserta en ese vector ... oops.

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 el llamador. Es necesario pensar en las condiciones previas correctas y las condiciones posteriores, y la vida útil del objeto es solo una pequeña parte de ese pastel más grande.

Los objetos no "mueren", algún trozo de código los destruye. Y lanzar shared_ptr en el problema en lugar de averiguar el contrato de la llamada es una seguridad falsa.

    
respondido por el Ben Voigt 04.02.2012 - 16:02
16

Prefiero no pensar en términos absolutos (como "último recurso") sino en relación con el dominio del problema.

C ++ puede ofrecer varias formas diferentes de administrar la vida útil. Algunos de ellos intentan volver a conducir los objetos de forma apilada. Algunos otros intentan escapar de esta limitación. Algunos de ellos son "literales", otros son aproximaciones.

En realidad puedes:

  1. usa la semántica de valor puro . Funciona para objetos relativamente pequeños donde lo importante son "valores" y no "identidades", donde puede suponer que dos Person que tienen un mismo name son la misma persona (mejor: dos representaciones de una misma persona ). La pila de máquinas otorga el tiempo de vida útil, al final no importa el programa (ya que una persona es su nombre , sin importar qué Person esté llevando it)
  2. utilice objetos asignados a la pila y referencias o indicadores relacionados: permite el polimorfismo y otorga vida útil al objeto. No hay necesidad de "punteros inteligentes", ya que garantiza que ningún objeto pueda ser "señalado" por estructuras que se quedan en la pila más tiempo que el objeto al que apuntan (primero cree el objeto, luego las estructuras que se refieren a él).
  3. use los objetos asignados al montón administrados por la pila : esto es lo que hacen std :: vector y todos los contenedores, y wat std::unique_ptr (puede pensarlo como un vector con tamaño 1). Nuevamente, admite que el objeto comienza a existir (y finaliza su existencia) antes (después) de la estructura de datos a la que hace referencia.

La debilidad de estos métodos es que los tipos de objetos y las cantidades 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" en su fuerza en todas las situaciones en las que la creación y eliminación de objetos son consecuencia de las actividades del usuario, de modo que el tipo de tiempo de ejecución del objeto no se conoce en tiempo de compilación y puede haber estructuras excesivas que se refieren a los objetos el usuario solicita eliminar de una llamada de función de nivel de pila más profunda. En estos casos, tienes que:

  • introduzca algo de disciplina sobre la gestión de objetos y las estructuras de referencia relacionadas o ...
  • ir de alguna manera al lado oscuro de "escapar de la vida útil basada en la pila pura": el objeto debe salir independientemente de las funciones que lo crearon. Y debe dejar ... hasta que sean necesarios .

C ++ isteslf no tiene ningún mecanismo nativo para monitorear ese evento ( while(are_they_needed) ), por lo tanto tienes que aproximarte con:

  1. usar propiedad compartida : la vida de los objetos está vinculada a un "contador de referencia": funciona si la "propiedad" se puede organizar jerárquicamente, falla cuando existen los bucles de propiedad. Esto es lo que hace std :: shared_ptr. Y weak_ptr puede usarse para romper el bucle. Esto funciona la mayor parte del tiempo pero falla en el diseño grande, donde muchos diseñadores trabajan en diferentes equipos y no hay una razón clara (algo que provenga de algún requisito) sobre quién debe ser el dueño de qué (el ejemplo típico son las cadenas con gustos dobles: es el ¿Anteriormente, la siguiente se refiere a la anterior o la siguiente, y la anterior se refiere a la siguiente? En caso de un requisito, las soluciones son equivalentes, y en un gran proyecto corre el riesgo de mezclarlas)
  2. Use un montón de recolección de basura : simplemente no le importa la vida útil. Ejecutas el coleccionista de vez en cuando y lo que es inaccesible se considera "ya no es necesario" y ... bueno ... ejem ... ¿se destruye? ¿finalizado? ¿congelado?. Hay una serie de recopiladores GC, pero nunca encuentro uno que sea realmente compatible con C ++. La mayoría de ellos tienen memoria libre, sin preocuparse por la destrucción de objetos.
  3. Utilice un recolector de basura compatible con C ++ , con una interfaz de métodos estándar adecuada. Buena suerte para encontrarlo.

Desde la primera solución hasta la última, la cantidad de estructura de datos auxiliar necesaria para administrar la vida útil del objeto aumenta, a medida que el tiempo dedicado a organizarlo y mantenerlo.

El recolector de basura tiene un costo, shared_ptr tiene menos, unique_ptr incluso menos, y los objetos administrados apilados 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 el std:: propuesto como último recurso. Pero puede ser la solución correcta, si está en la situación que expliqué.

    
respondido por el Emilio Garavaglia 07.02.2012 - 09:55
9

Lo único que mencionó Herb Sutter en una sesión posterior es que cada vez que copia un 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 la memoria no es insignificante. Dada la elección, es mejor usar un valor de pila o un unique_ptr<> y pasar referencias o punteros sin procesar.

    
respondido por el Eclipse 04.02.2012 - 19:02
7

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 punteros desnudos tienen su lugar.

Una cosa que todos acordaron es que estamos (desarrolladores, autores de libros, etc.) 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 libros seminales de C ++, como C ++ efectivo (Meyers) y Estándares de codificación de C ++ (Sutter & Alexandrescu), un par de años mientras que la industria está La experiencia y las mejores prácticas con C ++ 11 se amplían.

    
respondido por el Eddie Velasquez 04.02.2012 - 17:32
5

Creo que lo que está tratando de hacer es que cada vez es más común que todos escriban shared_ptr siempre que hayan escrito un puntero estándar (como un tipo de reemplazo global), y que se esté utilizando como un desecho en lugar de diseñar o al menos planear 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í sola no resuelve los problemas del ciclo. Aún puedes 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 usó para crear a1, a2 quedan fuera del alcance (por ejemplo, al final de una función). Ahora tiene una fuga, nadie más se refiere a a1 y a2, pero se refieren el uno al otro por lo que sus recuentos de ref son siempre 1, y ha filtrado.

Ese es el ejemplo simple, cuando esto ocurre en un código real, generalmente ocurre de manera complicada. Existe una solución con weak_ptr, pero muchas personas ahora simplemente comparten shared_ptr en todas partes y ni siquiera saben del problema de fugas o incluso de weak_ptr.

Para terminar: 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 sobre la creación de objetos, tiempos de vida y destrucción.

editar: incluso si eso significa "desconocido, necesito usar un shared_ptr", todavía lo has pensado y lo estás haciendo de manera intencional.

    
respondido por el anon 05.02.2012 - 22:22
2

Responderé desde mi experiencia con Objective-C, un lenguaje en el que todos los objetos se cuentan como referencia y se asignan en el montón. Debido a que tiene 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 de compiladores inteligentes como el reciente ARC (conteo automático de referencias).

Mi punto es que shared_ptr debería ser su primera opción en lugar del último recurso. Use el recuento 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.

    
respondido por el Dimitris 10.08.2012 - 18:28
1

Intentaré responder la pregunta:

  

¿Cómo podemos programar sin std :: shared_ptr y seguir administrando la vida útil de los objetos de forma segura?

C ++ tiene muchas formas diferentes de memorizar, por ejemplo:

  1. Use struct A { MyStruct s1,s2; }; en lugar de shared_ptr en el alcance de la clase. Esto es solo para programadores avanzados porque requiere que usted entienda cómo funcionan las dependencias y requiere la capacidad de controlar las dependencias lo suficiente 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 de c ++ incorporados nativos, pero su uso con clases definidas por el programador parece ser menos usado debido a estos problemas de dependencia y orden de clases. Esta solución también tiene problemas con sizeof. Los programadores ven los problemas en esto como un requisito para usar declaraciones hacia adelante o #incluidos innecesarios y, por lo tanto, muchos programadores recurrirán a una solución inferior de punteros y luego a shared_ptr.
  2. Use MyClass &find_obj(int i); + clone () en lugar de shared_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 mediante el uso de la asignación de almacenes de pila / libre, en lugar de una solución más simple basada en la pila o en el objeto. Una buena jerarquía de clases de C ++ admite todos los esquemas de administración de memoria, no solo uno de ellos. La solución basada en referencia puede funcionar si el objeto devuelto se almacena dentro del objeto contenedor, en lugar de usar la variable de alcance de la función local. Debe evitarse pasar la propiedad de la fábrica al código de usuario. Copiar el objeto después de usar find_obj () es una buena manera de manejarlo: los constructores de copia normales y el constructor normal (de diferente clase) con parámetro de referencia o clone () para objetos polimórficos pueden manejarlo.
  3. Uso de referencias en lugar de punteros o shared_ptrs. Cada clase de c ++ tiene constructores, y cada miembro de datos de referencia debe inicializarse. Este uso puede evitar muchos usos de punteros y shared_ptrs. Solo tiene que elegir si su memoria está dentro del objeto o fuera de él, y elegir la solución de estructura o la solución de referencia según la decisión. Los problemas con esta solución generalmente se relacionan con evitar los parámetros del constructor, lo cual es una práctica común pero problemática y malinterpretan cómo deben diseñarse las interfaces para las clases.
respondido por el tp1 04.02.2012 - 20:05

Lea otras preguntas en las etiquetas