2008-09-03 7 views
30

Als allgemeine Regel bevorzuge ich lieber Wert als Zeiger Semantik in C++ (dh mit vector<Class> anstelle von vector<Class*>). In der Regel wird der leichte Leistungsverlust mehr als wettgemacht, indem man nicht daran denken muss, dynamisch zugewiesene Objekte zu löschen.Kann ich in C++ polymorphe Container mit Wertesemantik haben?

Leider funktionieren Wertauflistungen nicht, wenn Sie eine Vielzahl von 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 (Wert Semantik) haben und es auch essen (polymorphe Container)? Oder muss ich Zeiger verwenden?

Antwort

22

Da die Objekte der verschiedenen Klassen unterschiedliche Größen haben, würden Sie am Ende in das Slicing-Problem geraten, wenn Sie sie als Werte speichern.

Eine vernünftige 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 std :: auto_ptr nicht ist.

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

+3

'boost :: ptr_vector' ist oft eine billigere und einfachere Alternative zu' std :: vector > ' – ben

+4

Diese Antwort bezieht sich nicht auf Wert Semantik. shared_ptr liefert Referenzsemantik über von T abgeleitete Klassen, für Instanz, shared_ptr a, b; b.reset (neu abgeleitet1); a = b; erstellt keine Kopie des Derived1-Objekts. – Aaron

+5

Ich habe nie gesagt, meine Lösung adressiert Wert Semantik. Ich sagte, es sei "eine vernünftige Lösung". Wenn Sie einen Weg kennen, um eine polymorphe Wertesemantik zu haben, dann steigen Sie direkt auf und sammeln Sie Ihren Nobelpreis. –

2

Werfen Sie einen Blick auf static_cast und reinterpret_cast
In C++ Programmiersprache, 3. Aufl, Bjarne Stroustrup es 130 auf Seite beschreibt Es gibt einen ganzen Abschnitt auf das in Kapitel 6.
Sie Neufassung können Ihre Elternklasse in die Kindklasse. Dies erfordert, dass Sie wissen, wann jeder welcher ist. In dem Buch spricht Dr. Stroustrup über verschiedene Techniken, um diese Situation zu vermeiden.

Tun Sie dies nicht. Dies negiert die Polymorphie, die Sie in erster Linie erreichen wollen!

3

Sie könnten auch in Betracht ziehen boost::any. Ich habe es für heterogene Container verwendet. Wenn Sie den Wert zurücklesen, müssen Sie einen any_cast durchführen. Es wird einen bad_any_cast werfen, wenn es fehlschlägt. Wenn das passiert, können Sie fangen und zum nächsten Typ übergehen.

I glaube es wird einen bad_any_cast werfen, wenn Sie versuchen, eine abgeleitete Klasse zu seiner Basis any_cast. 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 

Alles, was gesagt wird, würde ich auch zuerst die Shared_ptr Weg gehen! Ich dachte nur, das könnte von Interesse sein.

3

Die meisten Containertypen möchten die bestimmte Speicherstrategie abstrahieren, sei es verknüpfte Liste, Vektor, baumbasierte oder was Sie haben. Aus diesem Grund werden Sie Probleme haben, den oben genannten Kuchen zu besitzen und zu konsumieren (d. H. Der Kuchen ist Lüge (NB: jemand musste diesen Witz machen)).

Was also zu tun?Nun, es gibt ein paar nette Optionen, aber die meisten werden sich auf Varianten eines Themes oder einer Kombination von ihnen beschränken: einen geeigneten intelligenten Zeiger auswählen oder erfinden, auf eine clevere Art und Weise mit Vorlagen oder Vorlagenvorlagen spielen, unter Verwendung einer gemeinsamen Schnittstelle für Containern Das bietet einen Haken für die Implementierung von per-contained Doppelversand.

Es gibt grundlegende Spannung zwischen Ihren zwei erklärten Zielen, also sollten Sie entscheiden, was Sie wollen, dann versuchen Sie, etwas zu entwerfen, das Sie im Grunde erhält, was Sie wollen. Es ist ist möglich, einige nette und unerwartete Tricks zu tun, um Zeiger zu bekommen, um wie Werte mit klug genug Referenzzählung und clever genug Implementierungen einer Fabrik aussehen. Die Grundidee ist, Referenzzählung und Copy-on-Demand sowie Konstanz und (für den Faktor) eine Kombination aus Präprozessor, Templates und den statischen C++ - Initialisierungsregeln zu verwenden, um so viel wie möglich über die Automatisierung von Zeigerkonvertierungen zu erreichen.

Ich habe in der Vergangenheit einige Zeit versucht, sich vorzustellen, wie man virtuellen Proxy/Envelope-Letter/diesen niedlichen Trick mit Referenzzählern verwendet, um so etwas wie eine Basis für die semantische Wertprogrammierung in C++ zu erreichen.

Und ich denke, es könnte getan werden, aber Sie müssten eine ziemlich geschlossene, C# -verwaltete Code-ähnliche Welt innerhalb von C++ bereitstellen (obwohl eine, von der Sie bei Bedarf C++ durchbrechen könnten). Ich habe also viel Sympathie für deine Gedanken.

2

Nur um eine Sache zu allen 1800 INFORMATION bereits gesagt hinzuzufügen.

Vielleicht möchten Sie sehen "More Effective C++" von Scott Mayers "Punkt 3: Behandeln Sie nie Arrays polymorph", um dieses Problem besser zu verstehen.

10

Ja, Sie können.

Die Bibliothek boost.ptr_container bietet polymorphe semantische Wertversionen der Standardcontainer. Sie müssen nur einen Zeiger auf ein dem Heap zugewiesenes Objekt übergeben, und der Container übernimmt das Eigentum, und alle weiteren Operationen werden eine Wertesemantik liefern, mit Ausnahme der Rückforderung von Eigentumsrechten, die Ihnen fast alle Vorteile der Wertesemantik durch Verwendung eines intelligenten Zeigers bietet .

10

Ich wollte nur darauf hinweisen, dass Vektor <Foo> ist in der Regel effizienter als Vektor < Foo * >. In einem Vektor <Foo> sind alle Foos im Speicher nebeneinander angeordnet. Unter Annahme eines kalten TLB und Caches wird der erste Lesevorgang die Seite zu dem TLB hinzufügen und einen Teil des Vektors in die L # -Caches ziehen; Nachfolgende Lesevorgänge verwenden den Warm-Cache und den geladenen TLB mit gelegentlichen Cache-Fehlern und weniger häufigen TLB-Fehlern.

Vergleichen Sie dies mit einem Vektor < Foo * >: Wenn Sie den Vektor füllen, erhalten Sie Foo * s aus Ihrem Speicherzuordner. Angenommen, Ihr Allokator ist nicht besonders intelligent, (tcmalloc?) Oder Sie füllen den Vektor langsam im Laufe der Zeit, der Standort jedes Foo ist wahrscheinlich weit entfernt von den anderen Foos: vielleicht nur um Hunderte von Bytes, vielleicht Megabyte auseinander.

Im schlimmsten Fall, wie Sie durch ein Vektor-Scan < Foo * > und dereferencing jeden Zeiger Sie einen TLB Fehler und Cache-Miss entstehen werden - dies wird als ein viel langsamer am Ende wird, wenn Sie einen Vektor haben <Foo>. (Im wirklich schlimmsten Fall wurde jedes Foo auf die Festplatte ausgelagert, und bei jedem Lesevorgang werden die Suchlaufe seek() und read() benötigt, um die Seite wieder in den Arbeitsspeicher zu bringen.)

Also, weiter mit Vektor <Foo>, wann immer angemessen. :-)

+2

+1 für Cache-Betrachtung! Dieses Problem wird in Zukunft viel relevanter werden. –

+2

Es hängt davon ab, wie teuer Foo :: Foo (const Foo &) ist, da der Wert-Semantik-Container es auf Einfügungen aufrufen muss. – AndrewR

+0

Guter Punkt - aber solange Sie anhängen, ist es kein Problem. (Sie werden log2 (n) zusätzliche Kopien haben.) Auch die meisten Zugriffe sind Lesevorgänge und nicht Schreibvorgänge; Manchmal kann das gelegentliche teuere Schreiben immer noch ein Nettogewinn sein, indem es schneller liest. – 0124816

1

Ich benutze meine eigene Vorlagenkollektionsklasse mit exponierter Werttypsemantik, aber intern speichert sie Zeiger. Es verwendet eine benutzerdefinierte Iterator-Klasse, die bei der Dereferenzierung eine Wertreferenz anstelle eines Zeigers erhält. Beim Kopieren der Sammlung werden anstelle von duplizierten Zeigern tiefe Objektkopien erstellt, und hier liegt der meiste Aufwand (ein wirklich kleines Problem, überlegt, was ich stattdessen bekomme).

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