raw, weak_ptr, unique_ptr, shared_ptr etc… Wie wählt man sie mit Bedacht aus?

33

Es gibt viele Zeiger in C ++, aber um ehrlich zu sein, verwende ich in der C ++ - Programmierung (speziell mit dem Qt Framework) in 5 Jahren nur den alten rohen Zeiger:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Ich weiß, dass es viele andere "kluge" Hinweise gibt:

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Aber ich habe nicht die geringste Ahnung, was ich mit ihnen anfangen soll und was sie mir im Vergleich zu rohen Zeigern bieten können.

Zum Beispiel habe ich diesen Klassenheader:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Dies ist natürlich nicht erschöpfend, aber für jeden dieser drei Zeiger ist es in Ordnung, sie "roh" zu lassen, oder sollte ich etwas passenderes verwenden?

Und wenn ein Arbeitgeber beim zweiten Mal den Kodex liest, wird er dann streng darauf achten, welche Art von Zeigern ich verwende oder nicht?

CheshireChild
quelle
Dieses Thema scheint für SO so angemessen zu sein. Das war im Jahr 2008 . Und hier ist, welche Art von Zeiger verwende ich wann? . Ich bin sicher, dass Sie noch bessere Übereinstimmungen finden können. Dies waren nur die ersten, die ich gesehen habe
siehe
Ich bin der Grenzgänger, da es sowohl um die konzeptionelle Bedeutung / Absicht dieser Klassen als auch um die technischen Details ihres Verhaltens und ihrer Implementierung geht. Da sich die akzeptierte Antwort auf die erstere bezieht, bin ich froh, dass dies die "PSE-Version" dieser SO-Frage ist.
Ixrec

Antworten:

70

Ein "roher" Zeiger ist nicht verwaltet. Das heißt, die folgende Zeile:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... führt zu einem Speicherverlust, wenn eine Begleitung deletenicht zum richtigen Zeitpunkt ausgeführt wird.

auto_ptr

Um diese Fälle zu minimieren, std::auto_ptr<>wurde eingeführt. Aufgrund der Einschränkungen von C ++ vor dem Standard von 2011 ist es jedoch immer noch sehr einfach auto_ptr, Speicher zu verlieren. Es reicht jedoch für begrenzte Fälle wie diesen aus:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Einer der schwächsten Anwendungsfälle liegt in Behältern. Dies liegt daran auto_ptr<>, dass der Container möglicherweise den Zeiger löscht und Daten verliert, wenn eine Kopie von erstellt wird und die alte Kopie nicht sorgfältig zurückgesetzt wird.

unique_ptr

Als Ersatz führte C ++ 11 ein std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Solch ein unique_ptr<>wird korrekt bereinigt, auch wenn es zwischen Funktionen übergeben wird. Dies geschieht durch semantische Darstellung von "Besitz" des Zeigers - der "Besitzer" räumt auf. Dies macht es ideal für den Einsatz in Behältern:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

Im Gegensatz zu auto_ptr<>, unique_ptr<>ist hier gut erzogene, und wenn die vectorkomprimiert die Größe, keines der Objekte versehentlich während der gelöscht wird vectorkopiert seine Sicherungsspeicher.

shared_ptr und weak_ptr

unique_ptr<>Dies ist zwar nützlich, aber es gibt Fälle, in denen Sie möchten, dass zwei Teile Ihrer Codebasis auf dasselbe Objekt verweisen und den Zeiger herum kopieren können, wobei dennoch eine ordnungsgemäße Bereinigung gewährleistet ist. Ein Baum könnte beispielsweise so aussehen, wenn Sie Folgendes verwenden std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

In diesem Fall können wir sogar an mehreren Kopien eines Stammknotens festhalten, und der Baum wird ordnungsgemäß bereinigt, wenn alle Kopien des Stammknotens zerstört sind.

Dies funktioniert, weil jeder shared_ptr<>nicht nur den Zeiger auf das Objekt festhält, sondern auch eine Referenzanzahl aller shared_ptr<>Objekte, die auf denselben Zeiger verweisen. Wenn ein neuer erstellt wird, steigt die Anzahl. Wenn einer zerstört wird, sinkt die Zählung. Wenn die Zählung Null erreicht, ist der Zeiger deleted.

Dies führt also zu einem Problem: Doppelte verknüpfte Strukturen führen zu Zirkelverweisen. Angenommen, wir möchten parentunserem Baum einen Zeiger hinzufügen Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Wenn wir nun a entfernen Node, gibt es einen zyklischen Verweis darauf. Es wird niemals deleted sein, weil sein Referenzzähler niemals Null sein wird.

Um dieses Problem zu lösen, verwenden Sie ein std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Jetzt funktionieren die Dinge korrekt und das Entfernen eines Knotens hinterlässt keine starren Verweise auf den übergeordneten Knoten. Es macht das Laufen etwas komplizierter:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

Auf diese Weise können Sie einen Verweis auf den Knoten sperren, und Sie haben eine angemessene Garantie dafür, dass er nicht verschwindet, während Sie daran arbeiten, da Sie an einem shared_ptr<>davon festhalten.

make_shared und make_unique

Nun gibt es einige kleinere Probleme mit shared_ptr<>und unique_ptr<>die angegangen werden sollten. Die folgenden zwei Zeilen haben ein Problem:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Wenn thrower()eine Ausnahme ausgelöst wird, verlieren beide Zeilen Speicher. Darüber hinaus shared_ptr<>ist der Referenzzähler weit vom Objekt entfernt, auf das er zeigt, und dies kann eine zweite Zuordnung bedeuten. Das ist normalerweise nicht wünschenswert.

C ++ 11 bietet std::make_shared<>()und C ++ 14 bietet std::make_unique<>(), um dieses Problem zu lösen:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

In beiden Fällen tritt thrower()kein Speicherverlust auf , auch wenn eine Ausnahme ausgelöst wird. Als Bonus make_shared<>()haben Sie die Möglichkeit, den Referenzzähler im selben Speicherbereich wie das verwaltete Objekt zu erstellen. Dies kann schneller sein und ein paar Bytes Speicher einsparen, während Sie gleichzeitig eine Ausnahmesicherheitsgarantie erhalten!

Anmerkungen zu Qt

Es ist jedoch zu beachten, dass Qt, das Compiler vor C ++ 11 unterstützen muss, ein eigenes Garbage-Collection-Modell hat: Viele QObjects verfügen über einen Mechanismus, mit dem sie ordnungsgemäß zerstört werden, ohne dass der Benutzer dies deletetun muss.

Ich weiß nicht, wie sich QObjects verhalten wird, wenn es von verwalteten C ++ 11-Zeigern verwaltet wird, daher kann ich nicht sagen, dass dies shared_ptr<QDialog>eine gute Idee ist. Ich habe nicht genug Erfahrung mit Qt, um es sicher zu sagen, aber ich glaube, dass Qt5 für diesen Anwendungsfall angepasst wurde.

greyfade
quelle
1
@ Zilators: Bitte beachte meinen zusätzlichen Kommentar zu Qt. Die Antwort auf Ihre Frage, ob alle drei Zeiger verwaltet werden sollen, hängt davon ab, ob sich Qt-Objekte gut verhalten.
greyfade
2
msgstr "beide machen eine getrennte Zuordnung, um den Zeiger zu halten"? Nein, unique_ptr weist niemals etwas extra zu, nur shared_ptr muss einen Referenzzähler + Allokator-Objekt zuweisen. msgstr "beide Zeilen verlieren Speicher"? Nein, nur vielleicht, nicht einmal eine Garantie für schlechtes Benehmen.
Deduplizierer
1
@ Deduplicator: Mein Wortlaut muss unklar gewesen sein: Das shared_ptrist ein separates Objekt - eine separate Zuordnung - vom newed-Objekt. Sie existieren an verschiedenen Orten. make_sharedhat die Fähigkeit, sie am selben Ort zusammenzufügen, was unter anderem die Cache-Lokalität verbessert.
greyfade
2
@greyfade: Nononono. shared_ptrist ein Objekt. Und um ein Objekt zu verwalten, muss es ein (Referenzanzahl (schwach + stark) + Zerstörer) -Objekt zuweisen. make_sharedermöglicht die Zuordnung dieses und des verwalteten Objekts als ein Teil. unique_ptrverwendet diese nicht, daher gibt es keinen entsprechenden Vorteil, abgesehen davon, dass das Objekt immer im Besitz des Smart-Pointers ist. Als Nebenwirkung kann man hat eine , shared_ptrdie besitzt ein zugrunde liegendes Objekt und a nullptr, oder die nicht selbst tut und stellt einen nicht-Nullpointer.
Deduplizierer
1
Ich habe es mir angesehen, und es scheint eine allgemeine Verwirrung darüber zu geben, was a shared_ptrtut: 1. Es teilt sich den Besitz eines Objekts (dargestellt durch ein internes dynamisch zugewiesenes Objekt mit einer schwachen und einer starken Referenzanzahl sowie einem Deleter). . 2. Es enthält einen Zeiger. Diese beiden Teile sind unabhängig. make_uniqueund make_sharedbeide stellen sicher, dass das zugewiesene Objekt sicher in einem Smart-Pointer abgelegt wird. Darüber hinaus make_sharedermöglicht das Eigentum-Objekt und den verwalteten Zeiger zusammen zuordnet.
Deduplizierer