Puis-je avoir des conteneurs polymorphes avec une sémantique de valeur en C++ ?

StackOverflow https://stackoverflow.com/questions/41045

  •  09-06-2019
  •  | 
  •  

Question

En règle générale, je préfère utiliser la sémantique des valeurs plutôt que celle des pointeurs en C++ (c'est-à-dire utiliser vector<Class> au lieu de vector<Class*>).Habituellement, la légère perte de performances est largement compensée par le fait de ne pas avoir à penser à supprimer les objets alloués dynamiquement.

Malheureusement, les collections de valeurs ne fonctionnent pas lorsque vous souhaitez stocker une variété de types d'objets qui dérivent tous d'une base commune.Voir l'exemple ci-dessous.

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

Ma question est:Puis-je avoir mon gâteau (sémantique des valeurs) et le manger aussi (conteneurs polymorphes) ?Ou dois-je utiliser des pointeurs ?

Était-ce utile?

La solution

Étant donné que les objets de différentes classes auront des tailles différentes, vous finirez par rencontrer le problème du découpage si vous les stockez sous forme de valeurs.

Une solution raisonnable consiste à stocker des pointeurs intelligents sécurisés dans les conteneurs.J'utilise normalement boost::shared_ptr qui peut être stocké en toute sécurité dans un conteneur.Notez que std::auto_ptr ne l'est pas.

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr utilise le comptage de références afin de ne pas supprimer l'instance sous-jacente tant que toutes les références n'ont pas été supprimées.

Autres conseils

Oui, vous pouvez.

La bibliothèque boost.ptr_container fournit des versions sémantiques à valeurs polymorphes des conteneurs standard.Il vous suffit de transmettre un pointeur vers un objet alloué au tas, et le conteneur en prendra possession et toutes les opérations ultérieures fourniront une sémantique de valeur, à l'exception de la récupération de propriété, qui vous offre presque tous les avantages de la sémantique de valeur en utilisant un pointeur intelligent. .

Je voulais juste souligner que vector<Foo> est généralement plus efficace que vector<Foo*>.Dans un vecteur<Foo>, tous les Foos seront adjacents les uns aux autres en mémoire.En supposant un TLB et un cache froids, la première lecture ajoutera la page au TLB et extraira une partie du vecteur dans les caches L# ;les lectures ultérieures utiliseront le cache chaud et le TLB chargé, avec des échecs de cache occasionnels et des erreurs TLB moins fréquentes.

Comparez cela avec un vecteur<Foo*> :Au fur et à mesure que vous remplissez le vecteur, vous obtenez des Foo* de votre allocateur de mémoire.En supposant que votre allocateur n'est pas extrêmement intelligent (tcmalloc ?) ou que vous remplissez le vecteur lentement au fil du temps, l'emplacement de chaque Foo est susceptible d'être éloigné des autres Foos :peut-être juste par centaines d'octets, peut-être par mégaoctets d'intervalle.

Dans le pire des cas, lorsque vous parcourez un vecteur <Foo*> et que vous déréférencez chaque pointeur, vous subirez une erreur TLB et un échec de cache - cela finira par être un problème. parcelle plus lent que si vous aviez un vecteur<Foo>.(Eh bien, dans le pire des cas, chaque Foo a été paginé sur le disque, et chaque lecture entraîne une recherche de disque () et une lecture () pour déplacer la page dans la RAM.)

Alors, continuez à utiliser vector<Foo> chaque fois que cela est approprié.:-)

La plupart des types de conteneurs souhaitent faire abstraction de la stratégie de stockage particulière, qu'il s'agisse d'une liste chaînée, d'un vecteur, d'une arborescence ou autre.Pour cette raison, vous allez avoir du mal à la fois à posséder et à consommer le gâteau susmentionné (c'est-à-dire que le gâteau est un mensonge (NB :quelqu'un a dû faire cette blague)).

Alors que faire?Eh bien, il existe quelques options mignonnes, mais la plupart se résument à des variantes sur l'un des quelques thèmes ou combinaisons d'entre eux :choisir ou inventer un pointeur intelligent approprié, jouer avec des modèles ou des modèles de modèles d'une manière intelligente, en utilisant une interface commune pour les conteneurs qui fournit un crochet pour implémenter la double répartition par conteneur.

Il existe une tension fondamentale entre vos deux objectifs déclarés, vous devez donc décider de ce que vous voulez, puis essayer de concevoir quelque chose qui vous donne essentiellement ce que vous voulez.Il est Il est possible de faire des astuces intéressantes et inattendues pour que les pointeurs ressemblent à des valeurs avec un comptage de références suffisamment intelligent et des implémentations suffisamment intelligentes d'une usine.L'idée de base est d'utiliser le comptage de références, la copie à la demande et la constance et (pour le facteur) une combinaison du préprocesseur, des modèles et des règles d'initialisation statiques de C++ pour obtenir quelque chose d'aussi intelligent que possible en matière d'automatisation des conversions de pointeurs.

Dans le passé, j'ai passé du temps à essayer d'imaginer comment utiliser Virtual Proxy / Envelope-Letter / cette astuce mignonne avec des pointeurs comptés par référence pour accomplir quelque chose comme une base de programmation sémantique de valeurs en C++.

Et je pense que cela pourrait être fait, mais vous devrez fournir un monde assez fermé, semblable à du code géré par C#, au sein de C++ (bien qu'il soit possible de passer au C++ sous-jacent en cas de besoin).J'ai donc beaucoup de sympathie pour votre ligne de pensée.

Vous pourriez également envisager boost::tout.Je l'ai utilisé pour des conteneurs hétérogènes.Lors de la lecture de la valeur, vous devez effectuer un any_cast.Il lancera un bad_any_cast s'il échoue.Si cela se produit, vous pouvez attraper et passer au type suivant.

je croire il lancera un bad_any_cast si vous essayez de any_cast une classe dérivée vers sa base.Je l'ai essayé:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

Cela étant dit, j'emprunterais également la route shared_ptr en premier !Je pensais juste que cela pourrait être intéressant.

Jeter un coup d'œil à static_cast et réinterpréter_cast
Dans C++ Programming Language, 3e édition, Bjarne Stroustrup le décrit à la page 130.Il y a toute une section à ce sujet au chapitre 6.
Vous pouvez refondre votre classe Parent en classe Enfant.Cela nécessite que vous sachiez quand chacun est lequel.Dans le livre, le Dr.Stroustrup parle de différentes techniques pour éviter cette situation.

Ne faites pas cela.Cela annule le polymorphisme que vous essayez d’atteindre en premier lieu !

Juste pour ajouter une chose à tout 1800 RENSEIGNEMENTS déjà dit.

Vous voudrez peut-être jeter un oeil à "C++ plus efficace" par Scott Mayers « Point 3 :Ne traitez jamais les tableaux de manière polymorphe" afin de mieux comprendre ce problème.

J'utilise ma propre classe de collection basée sur un modèle avec une sémantique de type valeur exposée, mais en interne, elle stocke des pointeurs.Il utilise une classe d'itérateur personnalisée qui, lorsqu'elle est déréférencée, obtient une référence de valeur au lieu d'un pointeur.La copie de la collection crée des copies d'éléments approfondies, au lieu de pointeurs dupliqués, et c'est là que se situe la plupart des frais généraux (un problème vraiment mineur, compte tenu de ce que j'obtiens à la place).

C'est une idée qui pourrait répondre à vos besoins.

En cherchant une réponse à ce problème, je suis tombé sur ceci et une question similaire.Dans les réponses à l’autre question, vous trouverez deux solutions proposées :

  1. Utilisez std::facultatif ou boost::facultatif et un modèle de visiteur.Cette solution rend difficile l’ajout de nouveaux types, mais facilite l’ajout de nouvelles fonctionnalités.
  2. Utilisez une classe wrapper similaire à celle Sean Parent présente dans son discours.Cette solution rend difficile l’ajout de nouvelles fonctionnalités, mais facilite l’ajout de nouveaux types.

Le wrapper définit l'interface dont vous avez besoin pour vos classes et contient un pointeur vers l'un de ces objets.La mise en œuvre de l'interface se fait avec des fonctions gratuites.

Voici un exemple d'implémentation de ce modèle :

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

Différentes formes sont ensuite implémentées sous forme de structures/classes régulières.Vous êtes libre de choisir si vous souhaitez utiliser des fonctions membres ou des fonctions gratuites (mais vous devrez mettre à jour l'implémentation ci-dessus pour utiliser les fonctions membres).Je préfère les fonctions gratuites :

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

Vous pouvez maintenant ajouter les deux Circle et Rectangle objets au même std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

L’inconvénient de ce modèle est qu’il nécessite une grande quantité de passe-partout dans l’interface, puisque chaque fonction doit être définie trois fois.L'avantage est que vous obtenez une sémantique de copie :

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

Cela produit :

Drew rectangle with width 2
Drew rectangle with width 2

Si vous êtes préoccupé par le shared_ptr, vous pouvez le remplacer par un unique_ptr.Cependant, il ne sera plus copiable et vous devrez soit déplacer tous les objets, soit mettre en œuvre la copie manuellement.Sean Parent en discute en détail dans son exposé et une implémentation est présentée dans la réponse mentionnée ci-dessus.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top