Frage

Generell bevorzuge ich die Verwendung von Wert- statt Zeigersemantik in C++ (d. h. die Verwendung von vector<Class> anstatt vector<Class*>).Normalerweise wird der leichte Leistungsverlust dadurch mehr als ausgeglichen, dass nicht daran gedacht werden muss, dynamisch zugewiesene Objekte zu löschen.

Leider funktionieren Wertesammlungen nicht, wenn Sie verschiedene Objekttypen speichern möchten, die alle von einer gemeinsamen Basis abgeleitet sind.Siehe das Beispiel unten.

#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

}

Meine Frage ist:Kann ich meinen Kuchen haben (Werte-Semantik) und ihn auch essen (polymorphe Behälter)?Oder muss ich Zeiger verwenden?

War es hilfreich?

Lösung

Da die Objekte verschiedener Klassen unterschiedliche Größen haben, würden Sie am Ende auf das Slicing-Problem stoßen, wenn Sie sie als Werte speichern.

Eine sinnvolle Lösung besteht darin, Container-sichere Smart Pointer zu speichern.Normalerweise verwende ich boost::shared_ptr, das sicher in einem Container gespeichert werden kann.Beachten Sie, dass dies bei std::auto_ptr nicht der Fall ist.

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

shared_ptr verwendet die Referenzzählung, sodass die zugrunde liegende Instanz erst gelöscht wird, wenn alle Referenzen entfernt wurden.

Andere Tipps

Ja, du kannst.

Die Bibliothek boost.ptr_container stellt semantische Versionen der Standardcontainer mit polymorphen Werten bereit.Sie müssen nur einen Zeiger auf ein Heap-zugewiesenes Objekt übergeben, und der Container übernimmt den Besitz und alle weiteren Vorgänge stellen Wertesemantik bereit, mit Ausnahme der Rückforderung des Besitzes, wodurch Sie durch die Verwendung eines intelligenten Zeigers fast alle Vorteile der Wertesemantik nutzen können .

Ich wollte nur darauf hinweisen, dass vector<Foo> normalerweise effizienter ist als vector<Foo*>.In einem Vektor<Foo> liegen alle Foos im Speicher nebeneinander.Unter der Annahme eines kalten TLB und Cache fügt der erste Lesevorgang die Seite zum TLB hinzu und zieht einen Teil des Vektors in die L#-Caches;Nachfolgende Lesevorgänge verwenden den Warm-Cache und den geladenen TLB, mit gelegentlichen Cache-Fehlern und selteneren TLB-Fehlern.

Vergleichen Sie dies mit einem Vektor<Foo*>:Wenn Sie den Vektor füllen, erhalten Sie Foo*s von Ihrem Speicherzuteiler.Vorausgesetzt, Ihr Allokator ist nicht besonders intelligent (tcmalloc?) oder Sie füllen den Vektor im Laufe der Zeit langsam, ist die Position jedes Foo wahrscheinlich weit von den anderen Foo entfernt:vielleicht nur um Hunderte von Bytes, vielleicht Megabytes voneinander entfernt.

Im schlimmsten Fall kommt es zu einem TLB-Fehler und einem Cache-Fehler, wenn Sie einen Vektor<Foo*> durchsuchen und jeden Zeiger dereferenzieren viel langsamer als wenn Sie einen Vektor<Foo> hätten.(Naja, im wirklich schlimmsten Fall wurde jedes Foo auf die Festplatte ausgelagert, und bei jedem Lesevorgang ist ein Festplatten-Seek() und Read() erforderlich, um die Seite zurück in den RAM zu verschieben.)

Verwenden Sie also weiterhin vector<Foo>, wann immer es angebracht ist.:-)

Die meisten Containertypen möchten die jeweilige Speicherstrategie abstrahieren, sei es eine verknüpfte Liste, ein Vektor, ein baumbasierter Speicher oder was auch immer.Aus diesem Grund werden Sie Schwierigkeiten haben, den oben genannten Kuchen sowohl zu besitzen als auch zu konsumieren (d. h. der Kuchen ist gelogen (Anmerkung:irgendjemand musste diesen Witz machen)).

Was also tun?Nun, es gibt ein paar nette Optionen, aber die meisten beschränken sich auf Varianten eines oder mehrerer Themen oder Kombinationen davon:Einen geeigneten intelligenten Zeiger auswählen oder erfinden, auf clevere Weise mit Vorlagen oder Vorlagenvorlagen spielen und eine gemeinsame Schnittstelle für Container verwenden, die einen Haken für die Implementierung von Double-Dispatch pro Container bietet.

Zwischen Ihren beiden erklärten Zielen besteht eine grundsätzliche Spannung. Sie sollten also entscheiden, was Sie wollen, und dann versuchen, etwas zu entwerfen, das Ihnen im Grunde das bringt, was Sie wollen.Es Ist Es ist möglich, einige nette und unerwartete Tricks anzuwenden, um Zeiger mit ausreichend cleverer Referenzzählung und ausreichend cleveren Implementierungen einer Factory dazu zu bringen, wie Werte auszusehen.Die Grundidee besteht darin, Referenzzählung, Copy-on-Demand und Konstanz sowie (für den Faktor) eine Kombination aus Präprozessor, Vorlagen und den statischen Initialisierungsregeln von C++ zu verwenden, um eine möglichst intelligente Automatisierung der Zeigerkonvertierungen zu erreichen.

Ich habe in der Vergangenheit einige Zeit damit verbracht, mir vorzustellen, wie man Virtual Proxy / Envelope-Letter / diesen netten Trick mit Referenzzählzeigern verwenden kann, um so etwas wie eine Grundlage für wertsemantische Programmierung in C++ zu schaffen.

Und ich denke, es wäre machbar, aber man müsste eine ziemlich geschlossene, C#-verwaltete Code-ähnliche Welt innerhalb von C++ bereitstellen (allerdings eine, von der aus man bei Bedarf zum zugrunde liegenden C++ durchbrechen könnte).Daher habe ich großes Verständnis für Ihren Gedankengang.

Vielleicht denken Sie auch darüber nach boost::any.Ich habe es für heterogene Container verwendet.Beim Zurücklesen des Werts müssen Sie einen any_cast durchführen.Wenn es fehlschlägt, wird ein bad_any_cast ausgelöst.Wenn das passiert, können Sie es abfangen und mit dem nächsten Typ fortfahren.

ICH glauben Es wird ein bad_any_cast ausgelöst, wenn Sie versuchen, eine abgeleitete Klasse in ihre Basis zu übertragen.Ich versuchte es:

  // 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

Abgesehen davon würde ich auch zuerst die Route shared_ptr wählen!Ich dachte nur, das könnte von Interesse sein.

Schauen Sie mal rein static_cast Und reinterpret_cast
In C++ Programming Language, 3. Auflage, beschreibt Bjarne Stroustrup es auf Seite 130.Dazu gibt es einen ganzen Abschnitt in Kapitel 6.
Sie können Ihre übergeordnete Klasse in eine untergeordnete Klasse umwandeln.Dazu müssen Sie wissen, wann jeder welcher ist.In dem Buch schreibt Dr.Stroustrup spricht über verschiedene Techniken, um diese Situation zu vermeiden.

Mach das nicht.Dies negiert den Polymorphismus, den Sie überhaupt erreichen möchten!

Um allen noch eines hinzuzufügen 1800 INFORMATIONEN bereits gesagt.

Vielleicht möchten Sie einen Blick darauf werfen „Effektiveres C++“ von Scott Mayers „Punkt 3:Behandeln Sie Arrays niemals polymorph“, um dieses Problem besser zu verstehen.

Ich verwende meine eigene auf Vorlagen basierende Sammlungsklasse mit der Semantik des exponierten Werttyps, aber intern speichert sie Zeiger.Dabei wird eine benutzerdefinierte Iteratorklasse verwendet, die beim Dereferenzieren eine Wertreferenz anstelle eines Zeigers erhält.Durch das Kopieren der Sammlung entstehen tiefe Elementkopien statt duplizierter Zeiger, und hier liegt der meiste Overhead (ein wirklich kleines Problem, wenn man bedenkt, was ich stattdessen bekomme).

Das ist eine Idee, die Ihren Bedürfnissen entsprechen könnte.

Auf der Suche nach einer Antwort auf dieses Problem bin ich auf Folgendes gestoßen: eine ähnliche Frage.In den Antworten auf die andere Frage finden Sie zwei Lösungsvorschläge:

  1. Verwenden Sie std::optional oder boost::optional und ein Besuchermuster.Diese Lösung erschwert das Hinzufügen neuer Typen, erleichtert jedoch das Hinzufügen neuer Funktionen.
  2. Verwenden Sie eine ähnliche Wrapper-Klasse wie what Sean Parent stellt in seinem Vortrag vor.Diese Lösung macht es schwierig, neue Funktionen hinzuzufügen, aber es ist einfach, neue Typen hinzuzufügen.

Der Wrapper definiert die Schnittstelle, die Sie für Ihre Klassen benötigen, und enthält einen Zeiger auf ein solches Objekt.Die Implementierung der Schnittstelle erfolgt mit kostenlosen Funktionen.

Hier ist eine Beispielimplementierung dieses Musters:

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

Verschiedene Formen werden dann als reguläre Strukturen/Klassen implementiert.Sie können frei wählen, ob Sie Mitgliedsfunktionen oder kostenlose Funktionen verwenden möchten (Sie müssen jedoch die obige Implementierung aktualisieren, um Mitgliedsfunktionen verwenden zu können).Ich bevorzuge kostenlose Funktionen:

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

Sie können nun beides hinzufügen Circle Und Rectangle Gegenstände derselben 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;
}

Der Nachteil dieses Musters besteht darin, dass es eine große Menge an Boilerplate in der Schnittstelle erfordert, da jede Funktion dreimal definiert werden muss.Der Vorteil ist, dass Sie eine Kopiersemantik erhalten:

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

Dadurch entsteht:

Drew rectangle with width 2
Drew rectangle with width 2

Wenn Sie sich Sorgen darüber machen shared_ptr, Sie können es durch a ersetzen unique_ptr.Allerdings ist es dann nicht mehr kopierbar und Sie müssen entweder alle Objekte verschieben oder das Kopieren manuell durchführen.Sean Parent erörtert dies ausführlich in seinem Vortrag und eine Umsetzung wird in der oben genannten Antwort gezeigt.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top