smart shared_ptr example c++ pointers c++11 smart-pointers

shared_ptr - smart pointers c++



C++ 11 Semántica del puntero inteligente (4)

Antes de irme, como accidentalmente una novela ...

TL; DR Utilice punteros compartidos para resolver problemas de responsabilidad, pero tenga mucho cuidado con las relaciones cíclicas. Si yo fuera usted, usaría una tabla de punteros compartidos para almacenar sus activos, y todo lo que necesite esos punteros compartidos también debería usar un puntero compartido. Esto elimina la sobrecarga de los indicadores débiles para la lectura (ya que la sobrecarga en el juego es como crear un nuevo puntero inteligente 60 veces por segundo por objeto). También es el enfoque que mi equipo y yo tomamos, y fue súper efectivo. También dice que se garantiza que sus texturas sobrevivirán a los objetos, por lo que sus objetos no pueden eliminar las texturas si usan punteros compartidos.

Si pudiera lanzar mis 2 centavos, me gustaría contarles sobre una incursión casi idéntica que hice con punteros inteligentes en mi propio videojuego; tanto lo bueno como lo malo.

El código de este juego tiene un enfoque casi idéntico a su solución n. ° 2: una tabla llena de punteros inteligentes para mapas de bits.

Aunque tuvimos algunas diferencias; habíamos decidido dividir nuestra tabla de mapas de bits en 2 piezas: una para mapas de bits "urgentes" y otra para mapas de bits "fáciles". Los mapas de bits urgentes son mapas de bits que se cargan constantemente en la memoria y se usarían en el medio de la batalla, donde necesitamos la animación AHORA y no queremos ir al disco duro, que tiene un tartamudeo muy notable. La tabla fácil era una tabla de cadenas de rutas de archivos a los mapas de bits en el disco duro. Estos serían mapas de bits grandes cargados al comienzo de una sección relativamente larga del juego; como la animación para caminar de tu personaje o la imagen de fondo.

Usar punteros sin procesar aquí tiene algunos problemas, específicamente la propiedad. Mira, nuestra tabla de activos tenía una función Bitmap *find_image(string image_name) . Esta función buscaría primero en la tabla urgente la entrada que coincida con image_name . Si se encuentra, genial! Devuelve un puntero de mapa de bits. Si no se encuentra, busque en la tabla fácil. Si encontramos una ruta que coincida con su nombre de imagen, cree el mapa de bits y luego devuelva ese puntero.

La clase para usar esto más definitivamente fue nuestra clase de Animación. Aquí está el problema de la propiedad: ¿cuándo debería una animación eliminar su mapa de bits? Si proviene de la tabla fácil entonces no hay problema; ese mapa de bits fue creado específicamente para ti. ¡Es su deber eliminarlo!

Sin embargo, si su mapa de bits provino de la tabla urgente, no podría eliminarlo, ya que al hacerlo evitaría que otros lo usen, y su programa bajará como ET el juego, y sus ventas seguirán.

Sin punteros inteligentes, la única solución es hacer que la clase de Animación clone sus mapas de bits pase lo que pase. Esto permite una eliminación segura, pero mata la velocidad del programa. ¿No se supone que estas imágenes son sensibles al tiempo?

Sin embargo, si la clase de activos fuera a devolver un shared_ptr<Bitmap> , entonces no tiene de qué preocuparse. Nuestra tabla de activos estaba estática, así que esos indicadores duraron hasta el final del programa sin importar qué. Cambiamos nuestra función para ser shared_ptr<Bitmap> find_image (string image_name) , y nunca tuvimos que volver a clonar un mapa de bits. Si el mapa de bits provino de la tabla fácil, entonces ese puntero inteligente era el único de su tipo, y se eliminó con la animación. Si se trataba de un mapa de bits urgente, la tabla aún contenía una referencia después de la destrucción de la Animación, y los datos se conservaban.

Esa es la parte feliz, aquí está la parte fea.

He encontrado punteros compartidos y únicos para ser genial, pero definitivamente tienen sus advertencias. El más grande para mí es no tener control explícito sobre cuándo se borran sus datos. Los punteros compartidos salvaron nuestra búsqueda de activos, pero mataron al resto del juego en la implementación.

Mira, tuvimos una pérdida de memoria, y pensamos "¡deberíamos usar punteros inteligentes en todas partes!". Un gran error.

Nuestro juego tenía GameObjects , que estaban controlados por un Environment . Cada entorno tenía un vector de GameObject * , y cada objeto tenía un puntero a su entorno.

Deberías ver a dónde voy con esto.

Los objetos tenían métodos para "expulsarse" de su entorno. Esto sería en caso de que necesiten moverse a una nueva área, o tal vez teletransportarse, o pasar a través de otros objetos.

Si el entorno era el único titular de referencia del objeto, entonces su objeto no podría abandonar el entorno sin ser eliminado. Esto sucede comúnmente al crear proyectiles, especialmente teletransportar proyectiles.

Los objetos también borraban su entorno, al menos si eran los últimos en abandonarlo. El entorno para la mayoría de los estados del juego también era un objeto concreto. ¡FUIMOS LLAMANDO ELIMINAR EN LA PILA! Sí, éramos aficionados, nos demandan.

En mi experiencia, usa unique_pointers cuando eres demasiado perezoso para llamar a delete y solo una cosa poseerá tu objeto, usa shared_pointers cuando quieras que varios objetos apunten a una cosa, pero no puede decidir quién tiene que eliminarla, y tenga mucho cuidado con las relaciones cíclicas con shared_pointers.

He estado trabajando con punteros durante algunos años, pero hace poco decidí pasar a los punteros inteligentes de C ++ 11 (es decir, únicos, compartidos y débiles). He investigado un poco sobre ellos y estas son las conclusiones que he extraído:

  1. Punteros únicos son geniales. Administran su propia memoria y son tan livianos como punteros sin procesar. Prefiere unique_ptr sobre punteros sin procesar tanto como sea posible.
  2. Los indicadores compartidos son complicados. Tienen una sobrecarga significativa debido al conteo de referencias. Pásalos por referencia constante o lamenta el error de tus caminos. No son malvados, pero deberían usarse con moderación.
  3. Los punteros compartidos deben poseer objetos; use punteros débiles cuando no se requiera propiedad. Bloquear un weak_ptr tiene una sobrecarga equivalente al constructor de copia shared_ptr.
  4. Continúa ignorando la existencia de auto_ptr, que ahora está en desuso de todos modos.

Así que con estos principios en mente, me puse a revisar mi código base para utilizar nuestros nuevos y brillantes punteros inteligentes, con la intención de despejar a bordo tantos punteros crudos como sea posible. Sin embargo, me he confundido sobre la mejor forma de aprovechar los punteros inteligentes C ++ 11.

Supongamos, por ejemplo, que estamos diseñando un juego simple. Decidimos que es óptimo cargar un tipo de datos de Textura ficticio en una clase de TextureManager. Estas texturas son complejas y por lo tanto no es factible pasarlas por valor. Además, supongamos que los objetos del juego necesitan texturas específicas según su tipo de objeto (es decir, automóvil, bote, etc.).

Antes, habría cargado las texturas en un vector (u otro contenedor como unordered_map) y había almacenado punteros a estas texturas dentro de cada objeto del juego respectivo, de modo que pudieran referirse a ellas cuando necesitaran renderizarse. Supongamos que se garantiza que las texturas sobrevivirán a sus punteros.

Mi pregunta, entonces, es cómo utilizar mejor los indicadores inteligentes en esta situación. Veo algunas opciones:

  1. Almacene las texturas directamente en un contenedor, luego construya un unique_ptr en cada objeto del juego.

    class TextureManager { public: const Texture& texture(const std::string& key) const { return textures_.at(key); } private: std::unordered_map<std::string, Texture> textures_; }; class GameObject { public: void set_texture(const Texture& texture) { texture_ = std::unique_ptr<Texture>(new Texture(texture)); } private: std::unique_ptr<Texture> texture_; };

    Mi comprensión de esto, sin embargo, es que una nueva textura sería copiada a partir de la referencia aprobada, que luego sería propiedad de unique_ptr. Esto me parece muy indeseable, ya que tendría tantas copias de la textura como objetos de juego que la usan, derrotando el punto de punteros (sin juego de palabras).

  2. Almacene no las texturas directamente, sino sus punteros compartidos en un contenedor. Use make_shared para inicializar los punteros compartidos. Construir punteros débiles en los objetos del juego.

    class TextureManager { public: const std::shared_ptr<Texture>& texture(const std::string& key) const { return textures_.at(key); } private: std::unordered_map<std::string, std::shared_ptr<Texture>> textures_; }; class GameObject { public: void set_texture(const std::shared_ptr<Texture>& texture) { texture_ = texture; } private: std::weak_ptr<Texture> texture_; };

    A diferencia del caso unique_ptr, no tendré que copiar y construir las texturas, pero renderizar los objetos del juego es costoso ya que tendría que bloquear el weak_ptr cada vez (tan complejo como copiar y construir un nuevo shared_ptr).

Para resumir, mi comprensión es tal: si tuviera que usar punteros únicos, tendría que copiar y construir las texturas; alternativamente, si tuviera que usar punteros compartidos y débiles, tendría que copiar-construir esencialmente los punteros compartidos cada vez que se dibujara un objeto del juego.

Entiendo que los indicadores inteligentes son intrínsecamente más complejos que los indicadores crudos, por lo que tendré que sufrir una pérdida en alguna parte, pero ambos costos parecen más altos de lo que deberían ser.

¿Alguien podría señalarme en la dirección correcta?

Perdón por la larga lectura, y gracias por su tiempo!


Incluso en C ++ 11, los punteros crudos siguen siendo perfectamente válidos como referencias no propietarias de objetos. En su caso, usted está diciendo "Supongamos que las texturas están garantizadas para sobrevivir a sus punteros". Lo que significa que es perfectamente seguro utilizar punteros sin procesar para las texturas en los objetos del juego. Dentro del administrador de textura, almacene las texturas automáticamente (en un contenedor que garantice la ubicación constante en la memoria) o en un contenedor de unique_ptr s.

Si la garantía de sobrevivir el puntero no era válida, tendría sentido almacenar las texturas en shared_ptr en el administrador y usar shared_ptr s o weak_ptr s en los objetos del juego, dependiendo de la semántica de propiedad de los objetos del juego con respecto a las texturas. Incluso podría revertir eso - almacenar shared_ptr s en los objetos y weak_ptr s en el administrador. De esta forma, el administrador serviría como caché: si se solicita una textura y su weak_ptr sigue siendo válido, dará una copia del mismo. De lo contrario, cargará la textura, dará un shared_ptr y mantendrá un weak_ptr .


Para resumir su caso de uso: *) Se garantiza que los objetos sobrevivan a sus usuarios *) Los objetos, una vez creados, no se modifican (creo que esto está implícito en su código) *) Los objetos son referenciables por nombre y se garantiza que existen para cualquier nombre que su aplicación le pedirá (estoy extrapolando - Trataré a continuación qué hacer si esto no es cierto).

Este es un caso de uso encantador. ¡Puedes usar la semántica de valores para texturas en toda tu aplicación! Esto tiene las ventajas de un gran rendimiento y es fácil de razonar.

Una forma de hacerlo es que tu TextureManager devuelva Texture const *. Considerar:

using TextureRef = Texture const*; ... TextureRef TextureManager::texture(const std::string& key) const;

Debido a que el subyacente objeto Texture tiene la vida útil de su aplicación, nunca se modifica y siempre existe (su puntero nunca es nullptr) simplemente puede tratar su TextureRef como un valor simple. Puede pasarlos, devolverlos, compararlos y hacer contenedores de ellos. Son muy fáciles de razonar y muy eficientes para trabajar.

La molestia aquí es que tiene una semántica de valores (lo cual es bueno), pero una sintaxis de puntero (que puede ser confusa para un tipo con semántica de valores). En otras palabras, para acceder a un miembro de tu clase de Textura necesitas hacer algo como esto:

TextureRef t{texture_manager.texture("grass")}; // You can treat t as a value. You can pass it, return it, compare it, // or put it in a container. // But you use it like a pointer. double aspect_ratio{t->get_aspect_ratio()};

Una forma de lidiar con esto es usar algo como el idioma pimpl y crear una clase que no sea más que un contenedor para un puntero a una implementación de texturas. Esto requiere un poco más de trabajo porque terminará creando una API (funciones de miembros) para su clase de envoltura de texturas que se reenvía a la API de su clase de implementación. Pero la ventaja es que tienes una clase de textura con semántica de valores y sintaxis de valores.

struct Texture { Texture(std::string const& texture_name): pimpl_{texture_manager.texture(texture_name)} { // Either assert(pimpl_); // or if (not pimpl_) {throw /*an appropriate exception */;} // or do nothing if TextureManager::texture() throws when name not found. } ... double get_aspect_ratio() const {return pimpl_->get_aspect_ratio();} ... private: TextureImpl const* pimpl_; // invariant: != nullptr };

...

Texture t{"grass"}; // t has both value semantics and value syntax. // Treat it just like int (if int had member functions) // or like std::string (except lighter weight for copying). double aspect_ratio{t.get_aspect_ratio()};

Supuse que en el contexto de tu juego, nunca pedirás una textura que no se garantice que exista. Si ese es el caso, entonces puedes afirmar que el nombre existe. Pero si ese no es el caso, entonces debe decidir cómo manejar esa situación. Mi recomendación sería hacer una invariante de su clase contenedora que el puntero no puede ser nullptr. Esto significa que tira desde el constructor si la textura no existe. Eso significa que usted maneja el problema cuando intenta crear la Textura, en lugar de tener que buscar un puntero nulo cada vez que llama a un miembro de su clase contenedora.

En respuesta a su pregunta original, los punteros inteligentes son valiosos para la administración de por vida y no son particularmente útiles si todo lo que necesita es pasar las referencias al objeto cuya vida dura más que el puntero.


Puede tener un std :: map de std :: unique_ptrs donde se almacenan las texturas. Luego puede escribir un método get que devuelva una referencia a una textura por nombre. De esta forma, si cada modelo conoce el nombre de su textura (que debería), puede pasar el nombre al método get y recuperar una referencia del mapa.

class TextureManager { public: Texture& get_texture(const std::string& key) const { return *textures_.at(key); } private: std::unordered_map<std::string, std::unique_ptr<Texture>> textures_; };

A continuación, puede utilizar una textura en la clase de objeto del juego en lugar de una textura *, weak_ptr, etc.

De esta forma, el administrador de texturas puede actuar como un caché, el método get puede reescribirse para buscar la textura y, si se encuentra, lo regresa del mapa; de lo contrario, cárguelo primero, muévalo al mapa y luego devuélvalo.