Ist es eine gute Praxis, immer intelligente Zeiger zu verwenden?

80

Ich finde intelligente Zeiger viel komfortabler als rohe Zeiger. Ist es also eine gute Idee, immer intelligente Zeiger zu verwenden? (Bitte beachten Sie, dass ich aus Java stamme und daher die Idee der expliziten Speicherverwaltung nicht besonders mag. Wenn es also keine ernsthaften Leistungsprobleme mit intelligenten Zeigern gibt, möchte ich mich an diese halten.)

Hinweis: Obwohl ich aus Java stamme, verstehe ich die Implementierung von intelligenten Zeigern und die Konzepte von RAII recht gut. Sie können dieses Wissen also von meiner Seite als selbstverständlich betrachten, wenn Sie eine Antwort veröffentlichen. Ich verwende fast überall statische Zuordnung und verwende Zeiger nur bei Bedarf. Meine Frage ist nur: Kann ich immer intelligente Zeiger anstelle von rohen Zeigern verwenden?

Dony Borris
quelle
9
Die Verwendung des Wortes "immer" ist niemals eine gute Sache, wenn über bewährte Verfahren gesprochen wird, da es Umstände gibt, unter denen die Verwendung eines Musters oder einer Richtlinie aus zahlreichen Gründen nicht sinnvoll ist. M.
Max
@Neil Ja das habe ich nur gemeint.
Dony Borris
Ich meine das ohne Beleidigung, aber es ist klar, dass Sie ein gutes Buch bekommen und von vorne beginnen müssen. Ihre Terminologie ist falsch und ich fürchte, Ihr Code ist sehr "nicht C ++".
GManNickG
2
Verdammt, nichts als salzige C ++ - Programmierer hier
Bruno

Antworten:

79

Angesichts der verschiedenen Änderungen habe ich den Eindruck, dass eine umfassende Zusammenfassung nützlich wäre.

1. Wenn nicht

Es gibt zwei Situationen, in denen Sie keine intelligenten Zeiger verwenden sollten.

Das erste ist genau die gleiche Situation, in der Sie a nicht verwenden sollten C++ tatsächlich Klasse verwenden . IE: DLL-Grenze, wenn Sie dem Client den Quellcode nicht anbieten. Sagen wir anekdotisch.

Das zweite passiert viel häufiger: Smart Manager bedeutet Eigentum . Sie können Zeiger verwenden, um auf vorhandene Ressourcen zu verweisen, ohne deren Lebensdauer zu verwalten. Beispiel:

void notowner(const std::string& name)
{
  Class* pointer(0);
  if (name == "cat")
    pointer = getCat();
  else if (name == "dog")
    pointer = getDog();

  if (pointer) doSomething(*pointer);
}

Dieses Beispiel ist eingeschränkt. Ein Zeiger unterscheidet sich jedoch semantisch von einer Referenz darin, dass er auf eine ungültige Position (den Nullzeiger) verweisen kann. In diesem Fall ist es vollkommen in Ordnung, stattdessen keinen intelligenten Zeiger zu verwenden, da Sie die Lebensdauer des Objekts nicht verwalten möchten.

2. Intelligente Manager

Wenn Sie keine Smart Manager-Klasse schreiben und das Schlüsselwort verwenden, machen delete Sie etwas falsch.

Es ist eine kontroverse Sichtweise, aber nachdem ich so viele Beispiele für fehlerhaften Code überprüft habe, gehe ich kein Risiko mehr ein. Wenn Sie also schreiben new, benötigen Sie einen intelligenten Manager für den neu zugewiesenen Speicher. Und du brauchst es jetzt.

Das bedeutet nicht, dass Sie weniger Programmierer sind! Im Gegenteil, die Wiederverwendung von Code, der nachweislich funktioniert, anstatt das Rad immer wieder neu zu erfinden, ist eine Schlüsselkompetenz.

Nun beginnt die eigentliche Schwierigkeit: Welcher Smart Manager?

3. Intelligente Zeiger

Es gibt verschiedene intelligente Zeiger mit verschiedenen Eigenschaften.

Überspringen, std::auto_ptrdas Sie generell vermeiden sollten (die Kopiersemantik ist verschraubt).

  • scoped_ptr: kein Overhead, kann nicht kopiert oder verschoben werden.
  • unique_ptr: kein Overhead, kann nicht kopiert werden, kann verschoben werden.
  • shared_ptr/ weak_ptr: Ein Teil des Overheads (Referenzzählung) kann kopiert werden.

Versuchen Sie normalerweise, entweder scoped_ptroder zu verwenden unique_ptr. Wenn Sie mehrere Eigentümer benötigen, versuchen Sie, das Design zu ändern. Wenn Sie das Design nicht ändern können und wirklich mehrere Eigentümer benötigen, verwenden Sie a shared_ptr, aber achten Sie auf Referenzzyklen, die mit a unterbrochen werden solltenweak_ptr irgendwo in der Mitte .

4. Intelligente Container

Viele intelligente Zeiger sind nicht zum Kopieren gedacht, daher ist ihre Verwendung mit den STL-Containern etwas beeinträchtigt.

shared_ptrVerwenden Sie intelligente Container aus dem Boost Pointer Container, anstatt auf und deren Overhead zurückzugreifen . Sie emulieren die Schnittstelle klassischer STL-Container, speichern jedoch Zeiger, die sie besitzen.

5. Rollen Sie Ihre eigenen

Es gibt Situationen, in denen Sie möglicherweise Ihren eigenen Smart Manager rollen möchten. Stellen Sie sicher, dass Sie nicht nur einige Funktionen in den Bibliotheken verpasst haben, die Sie zuvor verwendet haben.

Das Schreiben eines intelligenten Managers bei Ausnahmen ist ziemlich schwierig. Normalerweise können Sie nicht davon ausgehen, dass Speicher verfügbar ist ( newmöglicherweise fehlschlägt) oder dass Copy Constructordie no throwGarantie besteht.

Es kann einigermaßen akzeptabel sein, die std::bad_allocAusnahme zu ignorieren und aufzuerlegen, dass Copy Constructors einer Reihe von Helfern nicht fehlschlagen ... schließlich ist boost::shared_ptrdies der Grund für den DParameter zum Löschen von Vorlagen.

Aber ich würde es nicht empfehlen, besonders für Anfänger. Es ist ein heikles Problem, und Sie werden die Fehler im Moment wahrscheinlich nicht bemerken.

6. Beispiele

// For the sake of short code, avoid in real code ;)
using namespace boost;

// Example classes
//   Yes, clone returns a raw pointer...
// it puts the burden on the caller as for how to wrap it
//   It is to obey the `Cloneable` concept as described in 
// the Boost Pointer Container library linked above
struct Cloneable
{
  virtual ~Cloneable() {}
  virtual Cloneable* clone() const = 0;
};

struct Derived: Cloneable
{
  virtual Derived* clone() const { new Derived(*this); }
};

void scoped()
{
  scoped_ptr<Cloneable> c(new Derived);
} // memory freed here

// illustration of the moved semantics
unique_ptr<Cloneable> unique()
{
  return unique_ptr<Cloneable>(new Derived);
}

void shared()
{
  shared_ptr<Cloneable> n1(new Derived);
  weak_ptr<Cloneable> w = n1;

  {
    shared_ptr<Cloneable> n2 = n1;          // copy

    n1.reset();

    assert(n1.get() == 0);
    assert(n2.get() != 0);
    assert(!w.expired() && w.get() != 0);
  } // n2 goes out of scope, the memory is released

  assert(w.expired()); // no object any longer
}

void container()
{
  ptr_vector<Cloneable> vec;
  vec.push_back(new Derived);
  vec.push_back(new Derived);

  vec.push_back(
    vec.front().clone()         // Interesting semantic, it is dereferenced!
  );
} // when vec goes out of scope, it clears up everything ;)
Matthieu M.
quelle
4
Gute Antwort! :) Ich denke, Herr Butterworth könnte etwas daraus lernen.
Dony Borris
2
Ich persönlich mag Neils Antworten (im Allgemeinen und diese im Besonderen). Ich dachte nur, dass das Thema eine ausführlichere Erklärung erfordert, da die Speicherverwaltung schwierig ist und wie "relativ" neu die Bibliotheken sind (ich denke hier an Pointer Container) , datiert 2007).
Matthieu M.
Was meinst du mit "Smart Managers"? Dieser Abschnitt der Antwort ergibt für mich keinen Sinn. Auch die Semantik vonstd::auto_ptr unterscheidet sich von dem, was die meisten Leute erwarten, aber sie ist sinnvoll und führt dazu, dass Designprobleme im Code vermieden werden. Die Aussage "generell vermeiden" ist unsinnig.
Frunsi
1
@frunsi: Wenn es Unsinn ist, "generell vermeiden" zu sagen, müssen Sie sich schnell an das ISO-Standardkomitee wenden und ihnen erklären, warum. Sie machen einen schrecklichen Fehler, indem sie auto_ptrin C ++ 0x veralten und stattdessen die Verwendung von empfehlen unique_ptr;-). Um fair zu sein, verlassen Sie unique_ptrsich auf die Erstellung / Zuweisung von Umzügen als Ersatz für auto_ptr. Wenn Sie strikt übertragbares Eigentum in C ++ 03 wünschen, haben Sie nicht viel Auswahl.
Steve Jessop
@Steve: In Ordnung! Es sieht so aus, als ob unique_ptr der richtige Weg ist, wenn Verschiebungssemantik verfügbar ist. Bis dahin ist auto_ptr immer noch nützlich, wird aber in ungefähr 100 Jahren entfernt;) Wenn wir alle nächstes Jahr anfangen, C ++ 0x-Code zu schreiben (ich hoffe es), sollten wir auto_ptr jetzt vermeiden, aber ich vermute es, also .. :) Nur ein Scherz, Sie haben Recht, es sollte jetzt vermieden werden.
Frunsi
18

Intelligente Zeiger führen eine explizite Speicherverwaltung durch. Wenn Sie nicht verstehen, wie sie ausgeführt werden, treten beim Programmieren mit C ++ große Probleme auf. Und denken Sie daran, dass Speicher nicht die einzige Ressource ist, die sie verwalten.

Um Ihre Frage zu beantworten, sollten Sie Smart-Pointer als erste Annäherung an eine Lösung bevorzugen, aber möglicherweise bereit sein, sie bei Bedarf fallen zu lassen. Sie sollten niemals Zeiger (oder irgendeine Art) oder dynamische Zuordnung verwenden, wenn dies vermieden werden kann. Zum Beispiel:

string * s1 = new string( "foo" );      // bad
string s2( "bar" );    // good

Bearbeiten: Um Ihre ergänzende Frage zu beantworten : "Kann ich immer intelligente Zeiger anstelle von Rohzeigern verwenden? Nein, können Sie nicht. Wenn Sie (zum Beispiel) Ihre eigene Version des Operators new implementieren müssen, müssen Sie dies tun." Lassen Sie es einen Zeiger zurückgeben, keinen intelligenten Zeiger.


quelle
10
Völlig nicht hilfreiche Antwort. Ich wünschte, ich hätte genug Repräsentanten, um dies abzulehnen.
Dony Borris
8
@Dony Die Qualität der Antwort spiegelt oft die der Frage wider.
12
@Dony Ich stimme im Allgemeinen falsch ab, anstatt einfach nicht hilfreiche Antworten. Schließlich kann es schwierig sein, genau zu wissen, was der Fragesteller lernen muss, um erleuchtet zu werden.
Philip Potter
4
Leider werden Neils Antworten oft mit einer Seite der Herablassung bedient. Er sollte aufhören, Fragen zu beantworten, die ihn frustrieren, weil die Welt nicht so klug oder erfahren ist wie er.
3
@Neil: Sie haben in Ihren Kommentaren mehrere Stöße auf der Kompetenzstufe des OP ausgeführt. Versteh mich aber nicht falsch. Ich bin total dafür. Ich denke, jeder, der den Sprachstandard nicht mindestens fünfmal von vorne nach hinten gelesen hat, befindet sich in einer "Welt voller Probleme beim Programmieren mit C ++".
13

Normalerweise sollten Sie keine Zeiger (intelligent oder anderweitig) verwenden, wenn Sie sie nicht benötigen. Machen Sie lokale Variablen, Klassenmitglieder, Vektorelemente und ähnliche Elemente besser zu normalen Objekten als zu Zeigern auf Objekte. (Da Sie aus Java kommen, sind Sie wahrscheinlich versucht, alles zuzuweisennew , was nicht empfohlen wird.)

Dieser Ansatz (" RAII ") erspart Ihnen die meiste Zeit die Sorge um Zeiger.

Wenn Sie Zeiger verwenden müssen, hängt dies von der Situation ab und warum genau Sie Zeiger benötigen. In der Regel können jedoch intelligente Zeiger verwendet werden. Es ist möglicherweise nicht immer (in Fettdruck) die beste Option, dies hängt jedoch von der jeweiligen Situation ab.

etw
quelle
4
Also, was sind diese Situationen, von denen "es abhängt"? Warum sagt es niemand?
P Shved
1
Ich kann mir einige Situationen vorstellen: Leistungsanforderungen machen gemeinsam genutzte Zeiger möglicherweise ungeeignet ( boost::scoped_ptrkönnten dann noch verwendet werden, aber vielleicht möchten Sie dann keine Abhängigkeit vom Boost haben?) - oder Sie müssen eine Schnittstelle mit einer C-API herstellen In diesem Fall sind Rohzeiger konsistenter. Wenn Sie über ein Array iterieren müssen, sind Ihre Iteratoren wahrscheinlich auch rohe Zeiger.
Jalf
1
Wenn Sie ein Objekt erstellen, das die Instanz, die es erstellt, überleben kann, können Sie es eindeutig nicht einfach in das andere Objekt einbetten.
Ben Voigt
1
@ Ben Voigt: Im allgemeinen Fall ist Ihr Beispiel nicht gültig. Sie können eine Rückkehr auto_ptr, unique_ptroder shared_ptrwie Sie einen Rohzeiger aus Ihrem Umfang Eigentumsübertragung passieren würde. scoped_ptrist der einzige intelligente Zeiger des Satzes, der nicht in der Lage sein wird, das Eigentum zu übertragen / zu teilen
David Rodríguez - dribeas
1
@Ben: Boost Shared Pointers haben einen Mechanismus, um dies zu beheben . Siehe diese Frage, die ich gestellt habe: stackoverflow.com/questions/1403465/… . Grundsätzlich können Sie einen gemeinsamen Zeiger haben, der eine Referenz (im Zählsinn) auf ein Objekt enthält, aber bei Referenzierung einen anderen Zeiger zurückgibt (in diesem Fall ein Mitglied des referenzierten Objekts).
Evan Teran
9

Eine gute Zeit nicht intelligenten Zeiger zu verwenden, liegt an der Schnittstellengrenze einer DLL. Sie wissen nicht, ob andere ausführbare Dateien mit demselben Compiler / denselben Bibliotheken erstellt werden. Die DLL-Aufrufkonvention Ihres Systems gibt nicht an, wie Standard- oder TR1-Klassen aussehen, einschließlich intelligenter Zeiger.

Wenn Sie in einer ausführbaren Datei oder Bibliothek den Besitz des Pointees darstellen möchten, sind intelligente Zeiger im Durchschnitt der beste Weg, dies zu tun. Es ist also in Ordnung, sie immer lieber als roh verwenden zu wollen. Ob Sie sie tatsächlich immer verwenden können, ist eine andere Frage.

Ein konkretes Beispiel, wenn Sie nicht annehmen, dass Sie eine Darstellung eines generischen Diagramms schreiben, wobei Scheitelpunkte durch Objekte und Kanten durch Zeiger zwischen den Objekten dargestellt werden. Die üblichen intelligenten Zeiger helfen Ihnen nicht weiter: Diagramme können zyklisch sein, und kein bestimmter Knoten kann für die Speicherverwaltung anderer Knoten verantwortlich gemacht werden, sodass gemeinsam genutzte und schwache Zeiger nicht ausreichen. Sie können beispielsweise alles in einen Vektor einfügen und Indizes anstelle von Zeigern verwenden oder alles in eine Deque einfügen und rohe Zeiger verwenden. Sie können verwenden, shared_ptrwenn Sie möchten, aber es wird nichts außer Overhead hinzugefügt. Oder Sie suchen nach Mark-Sweep-GC.

Ein Randfall: Ich bevorzuge es, wenn Funktionen einen Parameter als Zeiger oder Referenz verwenden und versprechen, keinen Zeiger oder Verweis darauf beizubehalten , anstatt einen zu nehmen, shared_ptrund Sie fragen sich, ob sie nach ihrer Rückkehr möglicherweise eine Referenz behalten, wenn Wenn Sie den Verweis immer wieder ändern, brechen Sie etwas usw. Wenn Verweise nicht beibehalten werden, wird dies häufig nicht explizit dokumentiert. Es versteht sich von selbst. Vielleicht sollte es nicht, aber es tut es. Intelligente Zeiger implizieren etwas über das Eigentum, und fälschlicherweise impliziert dies, dass dies verwirrend sein kann. Wenn Ihre Funktion also a übernimmt shared_ptr, müssen Sie unbedingt dokumentieren, ob eine Referenz beibehalten werden kann oder nicht.

Steve Jessop
quelle
6

In vielen Situationen glaube ich, dass sie definitiv der richtige Weg sind (weniger unordentlicher Bereinigungscode, geringeres Risiko von Lecks usw.). Es gibt jedoch einige sehr geringe zusätzliche Kosten. Wenn ich einen Code schreiben würde, der so schnell wie möglich sein muss (z. B. eine enge Schleife, die eine gewisse Zuweisung und eine freie Schleife durchführen muss), würde ich wahrscheinlich keinen intelligenten Zeiger verwenden, um etwas mehr Geschwindigkeit zu erreichen. Ich bezweifle jedoch, dass dies in den meisten Situationen einen messbaren Unterschied bewirken würde.

Mark Wilkins
quelle
6
Sie würden wahrscheinlich einen scoped_ptr mit einem Overhead von praktisch Null verwenden, wenn Sie Ressourcen in einer engen Schleife zuweisen, verwenden und zerstören würden (was möglicherweise überhaupt keine gute Idee ist). Wählen Sie die richtige Art von Smart Pointer für die Aufgabe.
Besucher
Wenn Sie das Objekt löschen müssen, gibt es keine tatsächliche Leistungseinbuße mit einigen intelligenten Zeigern (insbesondere scoped_ptr)
David Rodríguez - Dribeas
@ David: Das ist ein guter Punkt ... obwohl ich dachte (möglicherweise aus Unwissenheit), dass es in dieser Situation noch einen zusätzlichen Test geben würde. Ich muss eine Versammlung abladen und mich selbst unterrichten.
Mark Wilkins
2
shared_ptrSie müssen sowohl bei der Zuweisung des gemeinsam genutzten Informationsblocks als auch bei der Freigabe anhand dieser gemeinsam genutzten Informationen einen Overhead haben. Bei intelligenten Zeigern mit Einzelbesitz muss der Destruktor jedoch keinen Test durchführen: Löschen Sie einfach den internen Zeiger. Beispiele: libstdc ++ : ~auto_ptr() { delete _M_ptr; }, boost 1.37: ~scoped_ptr() { checked_delete(ptr); }Wo checked_deleteist eine Überprüfung der Kompilierungszeit deleteauf Typvollständigkeit und ein einzelner Aufruf von , der höchstwahrscheinlich inline sein wird.
David Rodríguez - Dribeas
@ David: Cool - danke, dass du das geteilt hast. Ich habe etwas gelernt
Mark Wilkins
4

Im Allgemeinen können Sie nicht immer intelligente Zeiger verwenden. Wenn Sie beispielsweise andere Frameworks verwenden, die keinen intelligenten Zeiger verwenden (wie Qt), müssen Sie auch Rohzeiger verwenden.

Jopa
quelle
2

Wenn Sie mit einer Ressource umgehen, sollten Sie immer RAII-Techniken verwenden. Bei Speicher bedeutet dies, dass Sie die eine oder andere Form eines Smart-Zeigers verwenden (Hinweis: Smart shared_ptr, wählen Sie nicht den Smart-Zeiger, der für Ihren speziellen Anwendungsfall am besten geeignet ist ). Nur so können Lecks bei Ausnahmen vermieden werden.

Es gibt immer noch Fälle, in denen Rohzeiger erforderlich sind, wenn die Ressourcenverwaltung nicht über den Zeiger erfolgt. Insbesondere sind sie die einzige Möglichkeit, eine rücksetzbare Referenz zu haben. Stellen Sie sich vor, Sie behalten eine Referenz in einem Objekt, dessen Lebensdauer nicht explizit behandelt werden kann (Mitgliedsattribut, Objekt im Stapel). Aber das ist ein sehr spezifischer Fall, den ich nur einmal in echtem Code gesehen habe. In den meisten Fällen ist die Verwendung von a shared_ptrein besserer Ansatz für die Freigabe eines Objekts.

David Rodríguez - Dribeas
quelle
2

Meine Einstellung zu intelligenten Zeigern: GROSS, wenn es schwierig ist zu wissen, wann eine Freigabe erfolgen kann (z. B. in einem Try / Catch-Block oder in einer Funktion, die eine Funktion (oder sogar einen Konstruktor!) Aufruft, die Sie aus Ihrer aktuellen Funktion herauswerfen könnte). oder Hinzufügen einer besseren Speicherverwaltung zu einer Funktion, die überall im Code zurückgegeben wird. Oder Zeiger in Container stecken.

Intelligente Zeiger verursachen jedoch Kosten, die Sie möglicherweise nicht für Ihr gesamtes Programm bezahlen möchten. Wenn die Speicherverwaltung einfach von Hand durchzuführen ist ("Hmm, ich weiß, dass ich diese drei Zeiger löschen muss, wenn diese Funktion endet, und ich weiß, dass diese Funktion vollständig ausgeführt wird"), warum sollten Sie dann die Zyklen verschwenden, die der Computer ausführen muss? es?

RyanWilcox
quelle
3
"Hmm, ich weiß, dass ich diese drei Zeiger löschen muss, wenn diese Funktion endet, und ich weiß, dass diese Funktion vollständig ausgeführt wird" - dafür auto_ptrist, oder scoped_ptr. Es ist selten, dass sie messbaren Overhead verursachen, und in der Zwischenzeit erleichtern sie es, den richtigen Code zu finden. Wenn beispielsweise das Erfassen des zweiten dieser drei Zeiger eine Ausnahme auslöst, geben Sie den ersten frei? Wie viel Code müssen Sie dafür schreiben, verglichen mit der Verwendung intelligenter Zeiger? Wie oft erwerben Sie wirklich Ressourcen, die Sie freigeben müssen, bei denen Ihre Akquisition jedoch nicht fehlschlagen kann?
Steve Jessop
Das ist ein weiteres gutes Beispiel dafür, wie man sich in einer Funktion befindet, die einen aus der aktuellen Funktion
herausholen
1

Ja, aber ich habe mehrere Projekte ohne die Verwendung eines intelligenten Zeigers oder Zeiger durchgeführt. Es ist empfehlenswert, Container wie Deque, List, Map usw. zu verwenden. Alternativ verwende ich Referenzen, wenn möglich. Anstatt einen Zeiger zu übergeben, übergebe ich eine Referenz oder eine konstante Referenz und es ist fast immer unlogisch, eine Referenz zu löschen / freizugeben, damit ich dort nie Probleme habe (normalerweise erstelle ich sie auf dem Stapel durch Schreiben{ Class class; func(class, ref2, ref3); }


quelle
0

Es ist. Smart Pointer ist einer der Eckpfeiler des alten Cocoa (Touch) -Ökosystems. Ich glaube, es wirkt sich immer wieder auf das Neue aus.

Trombe
quelle