38 votes

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

En règle générale, je préfère utiliser une sémantique de valeur plutôt que de pointeur en C++ (c'est-à-dire utiliser vector<Class> au lieu de vector<Class*> ). En général, la légère perte de performance est plus que compensée par le fait de ne pas avoir à se rappeler de supprimer les objets alloués dynamiquement.

Malheureusement, les collections de valeurs ne fonctionnent pas lorsque vous voulez 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 la suivante : puis-je avoir mon gâteau (sémantique des valeurs) et le manger aussi (conteneurs polymorphes) ? Ou dois-je utiliser des pointeurs ?

28voto

1800 INFORMATION Points 55907

Comme les objets de différentes classes auront des tailles différentes, vous finirez par vous heurter au problème du découpage en tranches si vous les stockez sous forme de valeurs.

Une solution raisonnable consiste à stocker des pointeurs intelligents dans des conteneurs. J'utilise normalement boost::shared_ptr qui est sûr à stocker 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, donc il ne supprimera pas l'instance sous-jacente tant que toutes les références ne sont pas supprimées.

3 votes

boost::ptr_vector est souvent une alternative moins coûteuse et plus simple que la std::vector<boost::shared_ptr<T>>

4 votes

Cette réponse n'aborde pas la sémantique des valeurs. shared_ptr<T> fournit la sémantique des références sur les classes dérivées de T, par exemple, shared_ptr<Base> a, b ; b.reset(new Derived1) ; a = b ; ne fait pas de copie de l'objet Derived1.

5 votes

Je n'ai jamais dit que ma solution abordait la sémantique des valeurs. J'ai dit que c'était "une solution raisonnable". Si vous connaissez un moyen d'avoir une sémantique de valeur polymorphe, alors approchez et recevez votre prix Nobel.

12voto

0124816 Points 804

Je voulais juste souligner que vector<Foo> est généralement plus efficace que vector<Foo*>. Dans un vector<Foo>, tous les Foos seront adjacents les uns aux autres en mémoire. En supposant une TLB et un cache froids, la première lecture ajoutera la page à la TLB et tirera un morceau du vecteur dans les caches L# ; les lectures suivantes utiliseront le cache chaud et la TLB chargée, avec des manques de cache occasionnels et des défauts de TLB moins fréquents.

Comparez cela à un vecteur<Foo*> : Lorsque 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, l'emplacement de chaque Foo est susceptible d'être très éloigné des autres Foo : peut-être seulement de quelques centaines d'octets, peut-être de mégaoctets.

Dans le pire des cas, en parcourant un vecteur<Foo*> et en déréférençant chaque pointeur, vous subirez un défaut de TLB et un manque de cache -- cela se traduira par un lot plus lent que si vous aviez un vecteur<Foo>. (Enfin, dans le cas vraiment le plus défavorable, chaque Foo a été paginé sur le disque, et chaque lecture entraîne un seek() et un read() sur le disque pour ramener la page en RAM).

Continuez donc à utiliser vector<Foo> chaque fois que cela est nécessaire :-)

4 votes

+1 pour la prise en compte du cache ! Ce problème deviendra beaucoup plus pertinent à l'avenir.

2 votes

Cela dépend du coût de Foo::Foo(const Foo&), car le conteneur de sémantique des valeurs devra l'appeler lors des insertions.

0 votes

Bon point - cependant, tant que vous ajoutez des éléments, ce n'est pas un problème. (Vous aurez log2(n) copies supplémentaires). De plus, la plupart des accès sont des lectures et non des écritures ; parfois, le fait de subir une écriture occasionnelle coûteuse peut toujours être un gain net en rendant les lectures plus rapides.

10voto

ben Points 826

Oui, vous pouvez.

La bibliothèque boost.ptr_container fournit des versions sémantiques de valeurs polymorphes des conteneurs standards. Vous n'avez qu'à passer un pointeur vers un objet alloué au tas, et le conteneur en prendra la propriété. Toutes les autres opérations fourniront la sémantique de la valeur, à l'exception de la récupération de la propriété, qui vous donne presque tous les avantages de la sémantique de la valeur en utilisant un pointeur intelligent.

5voto

Adam Hollidge Points 544

Vous pouvez également envisager boost::any . Je l'ai utilisé pour des conteneurs hétérogènes. Lorsque vous relisez la valeur, vous devez effectuer un any_cast. Si cela échoue, un bad_any_cast sera lancé. Si cela se produit, vous pouvez attraper et passer au type suivant.

I croire il lancera un bad_any_cast si vous essayez de any_cast une classe dérivée à 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

Tout ceci étant dit, j'opterais également pour la route des shared_ptr en premier ! J'ai juste pensé que cela pourrait être intéressant.

5voto

dragly Points 918

En cherchant une réponse à ce problème, je suis tombé sur ce document et sur celui de la Commission européenne. une question similaire . Dans les réponses à l'autre question, vous trouverez deux suggestions de solutions :

  1. Utilisez std::optional ou boost::optional et un modèle de visiteur. Cette solution rend difficile l'ajout de nouveaux types, mais facile l'ajout de nouvelles fonctionnalités.
  2. Utilisez une classe enveloppante similaire à ce que Sean Parent présente dans son exposé . Cette solution rend difficile l'ajout de nouvelles fonctionnalités, mais facile l'ajout de nouveaux types.

Le wrapper définit l'interface dont vous avez besoin pour vos classes et contient un pointeur vers un tel objet. L'implémentation de l'interface est faite avec des fonctions libres.

Voici un exemple de mise en œuvre 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;
};

Les différentes formes sont ensuite implémentées en tant que structs/classes ordinaires. Vous êtes libre de choisir si vous voulez utiliser les fonctions membres ou les fonctions libres (mais vous devrez mettre à jour l'implémentation ci-dessus pour utiliser les fonctions membres). Je préfère les fonctions libres :

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 des objets de la 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 texte 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 possible de le copier et vous devrez soit déplacer tous les objets, soit implémenter la copie manuellement. Sean Parent en parle en détail dans son exposé et une mise en œuvre est présentée dans la réponse mentionnée ci-dessus.

0 votes

Cela devrait vraiment être la réponse acceptée, car l'enveloppe que vous mentionnez (ce que j'ai commencé à appeler un "type de valeur polymorphe") est la solution la plus idiomatique et la plus puissante qui ait été proposée. std::any perd toute notion des concepts (comme la dérivation à partir d'une base commune expose) des éléments du conteneur. J'ai essentiellement fait la même suggestion que la vôtre dans ma réponse à une question similaire . Des types de valeurs polymorphes pour gagner !

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