37 votes

Sémantique des pointeurs intelligents C++11

Je travaille avec des pointeurs depuis quelques années maintenant, mais je n'ai décidé que très récemment de passer aux pointeurs intelligents de C++11 (à savoir unique, partagé et faible). J'ai effectué un certain nombre de recherches à leur sujet et voici les conclusions que j'en ai tirées :

  1. Les conseils uniques sont excellents. Ils gèrent leur propre mémoire et sont aussi légers que les pointeurs bruts. Préférez unique_ptr aux pointeurs bruts autant que possible.
  2. Les pointeurs partagés sont compliqués. Ils ont une surcharge significative due au comptage des références. Passez-les par référence constante ou regrettez l'erreur de vos méthodes. Ils ne sont pas mauvais, mais doivent être utilisés avec parcimonie.
  3. Les pointeurs partagés doivent posséder des objets ; utilisez des pointeurs faibles lorsque la propriété n'est pas nécessaire. Le verrouillage d'un pointeur faible a une surcharge équivalente à celle du constructeur de copie de pointeur partagé.
  4. Continuer à ignorer l'existence de auto_ptr, qui est maintenant déprécié de toute façon.

C'est donc avec ces principes à l'esprit que j'ai entrepris de réviser ma base de code afin d'utiliser nos nouveaux pointeurs intelligents, avec la ferme intention d'éliminer autant de pointeurs bruts que possible. Cependant, je ne sais plus comment tirer le meilleur parti des pointeurs intelligents de C++11.

Supposons, par exemple, que nous concevions un jeu simple. Nous décidons qu'il est optimal de charger un type de données Texture fictif dans une classe TextureManager. Ces textures sont complexes et il n'est donc pas envisageable de les transmettre par valeur. De plus, supposons que les objets du jeu ont besoin de textures spécifiques en fonction de leur type d'objet (c'est-à-dire voiture, bateau, etc.).

Auparavant, j'aurais chargé les textures dans un vecteur (ou un autre conteneur comme unordered_map) et stocké des pointeurs vers ces textures dans chaque objet de jeu respectif, de sorte qu'ils puissent s'y référer lorsqu'ils ont besoin d'être rendus. Supposons que les textures sont garanties pour survivre à leurs pointeurs.

Ma question est donc de savoir comment utiliser au mieux les pointeurs intelligents dans cette situation. Je vois quelques options :

  1. Stockez les textures directement dans un conteneur, puis construisez un unique_ptr dans chaque objet du jeu.

    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_;
    };

    Cependant, si j'ai bien compris, une nouvelle texture serait construite par copie à partir de la référence transmise, qui appartiendrait alors à l'unique_ptr. Cela me semble hautement indésirable, car j'aurais autant de copies de la texture que d'objets de jeu qui l'utilisent, ce qui irait à l'encontre de l'utilité des pointeurs (sans mauvais jeu de mots).

  2. Ne stocke pas les textures directement, mais leurs pointeurs partagés dans un conteneur. Utilisez make_shared pour initialiser les pointeurs partagés. Construire des pointeurs faibles dans les objets du jeu.

    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_;
    };

    Contrairement au cas de l'unique_ptr, je n'aurai pas à copier-construire les textures elles-mêmes, mais le rendu des objets du jeu est coûteux car je devrais verrouiller le weak_ptr à chaque fois (aussi complexe que de copier-construire un nouveau shared_ptr).

Pour résumer, ma compréhension est la suivante : si j'utilise des pointeurs uniques, je dois copier et construire les textures ; si j'utilise des pointeurs partagés et faibles, je dois essentiellement copier et construire les pointeurs partagés chaque fois qu'un objet du jeu doit être dessiné.

Je comprends que les pointeurs intelligents seront intrinsèquement plus complexes que les pointeurs bruts et que je devrai donc subir une perte quelque part, mais ces deux coûts semblent plus élevés qu'ils ne devraient l'être.

Quelqu'un peut-il m'indiquer la bonne direction ?

Désolé pour la longue lecture, et merci pour votre temps !

31voto

Angew Points 53063

Même en C++11, les pointeurs bruts sont toujours parfaitement valable comme des références non propriétaires à des objets. Dans votre cas, vous dites "supposons que les textures sont garanties de survivre à leurs pointeurs". Ce qui signifie que vous êtes parfaitement sûr d'utiliser des pointeurs bruts vers les textures dans les objets du jeu. À l'intérieur du gestionnaire de textures, stockez les textures soit automatiquement (dans un conteneur qui garantit un emplacement constant en mémoire), soit dans un conteneur d'objets de type unique_ptr s.

Si la garantie "outlive-the-pointer" était no valide, il serait logique de stocker les textures dans shared_ptr dans le gestionnaire et utiliser soit shared_ptr ou weak_ptr dans les objets du jeu, en fonction de la sémantique de propriété des objets du jeu par rapport aux textures. Vous pourriez même inverser cela - stocker shared_ptr dans les objets et weak_ptr dans le gestionnaire. De cette façon, le gestionnaire servirait de cache - si une texture est demandée et que sa texture est en cours de traitement, le gestionnaire peut être utilisé comme un cache. weak_ptr est toujours valide, il en donnera une copie. Sinon, il chargera la texture, émettra un shared_ptr et garder un weak_ptr .

11voto

Jon Kalb Points 103

Pour résumer votre cas d'utilisation : *) Les objets sont garantis de survivre à leurs utilisateurs *) Les objets, une fois créés, ne sont pas modifiés (je pense que cela est impliqué par votre code) *) Les objets peuvent être référencés par leur nom et leur existence est garantie pour tout nom que votre application demandera (j'extrapole - je traiterai plus loin de ce qu'il faut faire si ce n'est pas vrai).

C'est un cas d'utilisation délicieux. Vous pouvez utiliser la sémantique des valeurs pour les textures dans toute votre application ! Cela a l'avantage d'être très performant et facile à comprendre.

Une façon de le faire est que votre TextureManager renvoie une Texture const*. Pensez-y :

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

Comme l'objet Texture sous-jacent a la durée de vie de votre application, n'est jamais modifié et existe toujours (votre pointeur n'est jamais nullptr), vous pouvez traiter votre TextureRef comme une simple valeur. Vous pouvez les passer, les retourner, les comparer, et en faire des conteneurs. Il est très facile de raisonner sur eux et très efficace de travailler dessus.

L'ennui ici est que vous avez une sémantique de valeur (ce qui est bien), mais une syntaxe de pointeur (qui peut être déroutante pour un type avec une sémantique de valeur). En d'autres termes, pour accéder à un membre de votre classe Texture, vous devez faire quelque chose comme ceci :

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()};

Une façon de gérer cela est d'utiliser quelque chose comme l'idiome pimpl et de créer une classe qui n'est rien d'autre qu'une enveloppe d'un pointeur vers une implémentation de texture. Cela représente un peu plus de travail car vous devrez créer une API (fonctions membres) pour votre classe d'enveloppe de texture qui renvoie à l'API de votre classe d'implémentation. Mais l'avantage est que vous avez une classe de texture avec une sémantique et une syntaxe de valeur.

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()};

J'ai supposé que dans le contexte de votre jeu, vous ne demanderez jamais une texture dont l'existence n'est pas garantie. Si c'est le cas, vous pouvez simplement affirmer que le nom existe. Mais si ce n'est pas le cas, vous devez décider comment gérer cette situation. Ma recommandation serait de faire un invariant de votre classe wrapper que le pointeur ne peut pas être nullptr. Cela signifie que vous lancez le constructeur si la texture n'existe pas. Cela signifie que vous gérez le problème lorsque vous essayez de créer la texture, plutôt que de devoir vérifier si le pointeur est nul à chaque fois que vous appelez un membre de votre classe enveloppe.

Pour répondre à votre question initiale, les pointeurs intelligents sont précieux pour la gestion de la durée de vie et ne sont pas particulièrement utiles si tout ce dont vous avez besoin est de transmettre des références à un objet dont la durée de vie est garantie pour dépasser le pointeur.

3voto

const_ref Points 1615

Vous pourriez avoir une std::map de std::unique_ptrs où les textures sont stockées. Vous pourriez ensuite écrire une méthode get qui renvoie une référence à une texture par son nom. De cette façon, si chaque modèle connaît le nom de sa texture (ce qui devrait être le cas), vous pouvez simplement passer le nom dans la méthode get et récupérer une référence dans la map.

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_;
};

Vous pourriez alors simplement utiliser une Texture dans la classe d'objet du jeu plutôt qu'une Texture*, weak_ptr, etc.

De cette façon, le gestionnaire de textures peut agir comme un cache, la méthode get peut être réécrite pour rechercher la texture et si elle est trouvée, la renvoyer depuis la carte, sinon la charger d'abord, la déplacer vers la carte et ensuite renvoyer une référence à celle-ci

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X