2009-02-02 6 views
12

Ich habe verschiedene Smart Pointer-Implementierungen evaluiert (wow, es gibt eine Menge da draußen) und es scheint mir, dass die meisten von ihnen in zwei breite Klassifikationen kategorisiert werden können:Was ist der beste Weg, um intelligente Zeiger in C++ zu implementieren?

1) Diese Kategorie verwendet Vererbung auf der Objekte, auf die verwiesen wird, so dass sie Referenzzählungen haben und normalerweise up() und down() (oder deren Äquivalente) implementiert sind. IE, um den intelligenten Zeiger zu verwenden, müssen die Objekte, auf die Sie zeigen, von einer Klasse erben, die die ref-Implementierung bereitstellt.

2) Diese Kategorie verwendet ein sekundäres Objekt, um die Referenzzählungen zu speichern. Zum Beispiel, anstatt den Smart Pointer direkt auf ein Objekt zu richten, zeigt es tatsächlich auf dieses Metadatenobjekt ... Wer hat eine Referenzzählung und up() und down() Implementierungen (und wer stellt normalerweise einen Mechanismus für den Zeiger zur Verfügung Holen Sie sich das tatsächliche Objekt, auf das gezeigt wird, so dass der intelligente Zeiger operator ->() korrekt implementieren kann.

Nun hat 1 den Nachteil, dass alle Objekte, auf die Sie verweisen möchten, von einem gemeinsamen Vorgänger geerbt werden. Dies bedeutet, dass Sie damit nicht auf Zählobjekte verweisen können, die Sie nicht steuern können über den Quellcode zu.

2 hat das Problem, dass, da die Zählung in einem anderen Objekt gespeichert ist, wenn Sie jemals eine Situation haben, dass ein Zeiger auf ein vorhandenes Referenzobjekt in eine Referenz konvertiert wird, haben Sie wahrscheinlich einen Fehler (IE, da der count befindet sich nicht im eigentlichen Objekt, es gibt keine Möglichkeit, dass die neue Referenz den count bekommt ... ref ref copy Konstruktion oder Zuweisung ist in Ordnung, weil sie das count-Objekt teilen können, aber wenn Sie jemals von einem konvertieren müssen Zeiger, du bist total abgespritzt) ...

Jetzt, wie ich es verstehe, verwendet boost :: shared_pointer Mechanismus 2, oder so ähnlich ... Das sagt, ich kann mich nicht ganz entscheiden, was ist schlimmer! Ich habe immer nur Mechanismus 1 im Produktionscode verwendet ... Hat jemand Erfahrung mit beiden Stilen? Oder vielleicht gibt es einen anderen Weg, der besser ist als diese beiden?

Antwort

27

"Was ist der beste Weg, intelligente Zeiger in C++ zu implementieren"

  1. Bloß nicht! Verwenden Sie einen vorhandenen, gut getesteten Smartpointer, z. B. boost :: shared_ptr oder std :: tr1 :: shared_ptr (std :: unique_ptr und std :: shared_ptr mit C++ 11)
  2. Wenn Sie müssen, dann denken Sie daran:
    1. Verwendung safe-Bool Idiom
    2. einen betreiber liefern>
    3. die starke Ausnahme Garantie bieten
    4. Dokument der Ausnahme Anforderungen Ihre Klasse auf dem deleter macht
    5. Verwendung copy-modify-Swap soweit möglich die starke Ausnahme Gua zu implementieren RANTIE
    6. Dokument, ob Sie richtig Multithreading Griff
    7. Schreib umfangreiche Komponententests
    8. Umwandlung-Basis implementieren derart, dass sie auf dem abgeleiteten Zeigertyp (policied intelligente Zeiger/dynamische deleter intelligente Zeiger) löscht
    9. Unterstützung erhält Zugang zu Rohzeiger
    10. betrachten Sie Ihre Kosten/benifit schwachen Zeigers der Bereitstellung von Zyklen zu brechen
    11. geeigneten Guss Operatoren für Ihren intelligenten Zeiger
    12. liefern machen Konstruktor templated, um das Konstruieren des Basiszeigers von abgeleitet zu behandeln.

Und vergessen Sie nicht alles, was ich in der oben unvollständige Liste vergessen haben.

7

Ich benutze boost :: shared_ptr schon seit einigen Jahren und während Sie Recht haben über den Nachteil (keine Zuweisung über Zeiger möglich), ich denke, es war es definitiv wert wegen der großen Menge von Zeiger-Bugs Es hat mich gerettet.

In meiner Homebrew Game Engine habe ich normale Zeiger so oft wie möglich durch shared_ptr ersetzt. Der Leistungseinbruch, der dadurch verursacht wird, ist eigentlich nicht so schlecht, wenn Sie die meisten Funktionen als Referenz aufrufen, so dass der Compiler nicht zu viele temporäre shared_ptr-Instanzen erstellen muss.

+1

Darüber hinaus ist die Boost-Version von shared_ptr in TR1 migriert und wird so schließlich Standard-C++ - Bibliothek sein. –

+1

Ein Leistungsvergleich von Boost Smart Pointer ist hier: http://www.boost.org/doc/libs/1_39_0/libs/smart_ptr/smarttests.htm –

1

Das Problem mit 2 kann herum gearbeitet werden. Boost bietet boost :: shared_from_this aus dem gleichen Grund an. In der Praxis ist das kein großes Problem.

Aber der Grund, warum sie mit Ihrer Option # 2 ging, ist, dass es in allen Fällen verwendet werden kann. Es ist nicht immer eine Option, sich auf die Vererbung zu verlassen, und dann bleibt Ihnen ein intelligenter Zeiger, den Sie nicht für die Hälfte Ihres Codes verwenden können.

Ich würde sagen, # 2 ist am besten, einfach weil es unter allen Umständen verwendet werden kann.

3

Boost hat auch einen intrusiven Zeiger (wie Lösung 1), der nichts erbt. Es erfordert jedoch, den Zeiger auf die Klasse zu ändern, um die Referenzzählung zu speichern und entsprechende Elementfunktionen bereitzustellen. Ich habe dies in Fällen verwendet, in denen die Speichereffizienz wichtig war, und wollte nicht den Overhead eines anderen Objekts für jeden verwendeten gemeinsamen Zeiger verwenden.

Beispiel:

class Event { 
public: 
typedef boost::intrusive_ptr<Event> Ptr; 
void addRef(); 
unsigned release(); 
\\ ... 
private: 
unsigned fRefCount; 
}; 

inline void Event::addRef() 
{ 
    fRefCount++; 
} 
inline unsigned Event::release(){ 
    fRefCount--; 
    return fRefCount; 
} 

inline void intrusive_ptr_add_ref(Event* e) 
{ 
    e->addRef(); 
} 

inline void intrusive_ptr_release(Event* e) 
{ 
    if (e->release() == 0) 
    delete e; 
} 

Die PTR typedef verwendet wird, so dass ich leicht kann switcth zwischen boost :: shared_ptr <> und boost :: intrusive_ptr <> ohne jeden Client-Code zu ändern

3

Wenn Sie kleben mit denen, die in der Standard-Bibliothek sind, wird es dir gut gehen.
Es gibt zwar ein paar andere Typen als die von Ihnen angegebenen.

  • Geteilt: Wo ein Objekt besitzt, das Objekt aber Übertragung erlaubt: Wo das Eigentum zwischen mehreren Objekten
  • Besitzer geteilt wird.
  • Unbeweglich: Wo ein Objekt das Objekt besitzt und es nicht übertragen werden kann.

Die Standardbibliothek hat:

  • std :: auto_ptr

-Boost ein paar mehr hat, als haben von TR1 (nächste Version des Standards) angepasst

  • std :: tr1 :: shared_ptr
  • std :: tr1 :: schwacher_ptr

Und diejenigen, die immer noch im Boost sind (was relativ ein Muss ist), die es hoffentlich in tr2 schaffen.

  • boost :: scoped_ptr
  • boost :: scoped_array
  • boost :: shared_array
  • boost :: intrusive_ptr

See: Smart Pointers: Or who owns you baby?

+0

Ich glaube nicht, dass boost :: scoped_ptr es in gemacht hat tr1, so ist es immer noch boost :: scoped_ptr, nicht std :: tr1 :: scoped_ptr. –

+0

Upps. Mein Fehler. –

2

Es scheint mir dieser Frage ist eine Art wie fragen: "Welches ist der beste Sortieralgorithmus?" Es gibt keine einzige Antwort, es hängt von deinen Umständen ab.

Für meine eigenen Zwecke verwende ich Ihren Typ 1. Ich habe keinen Zugriff auf die TR1-Bibliothek. Ich habe die vollständige Kontrolle über alle Klassen, zu denen ich Zeiger weitergeben muss. Die zusätzliche Speicher- und Zeiteffizienz von Typ 1 mag ziemlich gering sein, aber Speicherverbrauch und Geschwindigkeit sind große Probleme für meinen Code, also war Typ 1 ein Slam-Dunk.

Auf der anderen Seite, würde ich für jeden, der TR1 verwenden kann, denke, dass die Typ 2 std :: tr1 :: shared_ptr Klasse eine vernünftige Standardwahl wäre, die immer dann verwendet wird, wenn es keinen zwingenden Grund gibt um es zu benutzen.

9

einfach eine andere Sicht auf die allgegenwärtige Boost-Antwort zu geben (auch wenn es die richtige Antwort für viele Anwendungen ist), nehmen Sie einen Blick auf Loki ‚s Implementierung von Smart Pointer. Für einen Diskurs über die Designphilosophie schrieb der ursprüngliche Schöpfer von Loki das Buch Modern C++ Design.

+0

+1 seit Boost nicht bieten deep_copy-Option für die Smart-Zeiger und ich denke, das ist eine Schande. – n1ckp

1

Unser Projekt verwendet ausgiebig Smartpointer. Am Anfang herrschte Unsicherheit darüber, welcher Zeiger verwendet werden sollte, und so wählte einer der Hauptautoren einen intrusiven Zeiger in seinem Modul und der andere eine nicht intrusive Version.

Im Allgemeinen waren die Unterschiede zwischen den beiden Zeigertypen nicht signifikant. Die einzige Ausnahme ist, dass frühe Versionen unserer non-intrusive Zeiger implizit aus einem rohen Zeiger umgewandelt und dies kann leicht zu Speicherproblemen führen, wenn die Zeiger falsch verwendet werden:

void doSomething (NIPtr<int> const &); 

void foo() { 
    NIPtr<int> i = new int; 
    int & j = *i; 
    doSomething (&j);   // Ooops - owned by two pointers! :(
} 

Vor einiger Zeit einig Refactoring führten in einigen Teile des Codes werden zusammengeführt, und so musste eine Auswahl getroffen werden, welcher Zeigertyp verwendet werden soll. Der nichtinvasive Zeiger hatte jetzt den konvertierenden Konstruktor als explizit deklariert und so wurde entschieden, mit dem intrusiven Zeiger zu gehen, um den Umfang der erforderlichen Codeänderung zu sparen.

Zu unserer großen Überraschung war eine Sache, die wir bemerkten, dass wir eine sofortige Leistungsverbesserung durch Verwendung des intrusiven Zeigers hatten. Wir haben nicht viel geforscht und angenommen, dass der Unterschied die Kosten für die Wartung des Zählobjekts sind. Es ist möglich, dass andere Implementierungen des nicht intrusiven geteilten Zeigers dieses Problem inzwischen gelöst haben.

+0

Aus diesem Grund habe ich begonnen, in jeder Klasse, die mit einem intelligenten Zeiger verwendet wird, einen intelligenten Zeiger zu schreiben. Dann kann die Entscheidung auf einer Klassenbasis getroffen und geändert werden, ohne den Client-Code zu beeinflussen. Dies funktioniert nur, wenn beide Zeigertypen dieselbe Schnittstelle haben. – KeithB

1

Worüber Sie sprechen sind intrusive und nicht intrusive Smartpointer. Boost hat beides. boost::intrusive_ptr ruft eine Funktion auf, um die Referenzzahl Ihres Objekts zu verringern und zu erhöhen, immer wenn die Referenzzählung geändert werden muss. Es ruft keine Mitgliedsfunktionen auf, sondern freie Funktionen. So können Objekte verwaltet werden, ohne dass die Definition ihrer Typen geändert werden muss. Und wie Sie sagen, boost::shared_ptr ist nicht aufdringlich, Ihre Kategorie 2.

Ich habe eine Antwort erklären intrusive_ptr: Making shared_ptr not use delete. Kurz gesagt, Sie verwenden es, wenn Sie ein Objekt haben, das bereits eine Referenzzählung hat oder (wie Sie erklären) ein Objekt benötigen, auf das bereits ein intrusive_ptr gehört.