Verwenden intelligenter Zeiger für Klassenmitglieder

159

Ich habe Probleme, die Verwendung intelligenter Zeiger als Klassenmitglieder in C ++ 11 zu verstehen. Ich habe viel über intelligente Zeiger gelesen und glaube zu verstehen, wie unique_ptrund shared_ptr/ oder wie ich weak_ptrim Allgemeinen arbeite. Was ich nicht verstehe, ist die tatsächliche Verwendung. Es scheint, als würde jeder empfehlen unique_ptr, fast immer den Weg zu gehen. Aber wie würde ich so etwas umsetzen:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Angenommen, ich möchte die Zeiger durch intelligente Zeiger ersetzen. A unique_ptrwürde wegen nicht funktionieren getDevice(), oder? Das ist also die Zeit, in der ich shared_ptrund benutze weak_ptr? Keine Möglichkeit zu benutzen unique_ptr? Scheint mir, dass es in den meisten Fällen shared_ptrsinnvoller ist, wenn ich keinen Zeiger in einem wirklich kleinen Bereich verwende?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Ist das der richtige Weg? Vielen Dank!

Michaelk
quelle
4
Es hilft, wirklich klar zu sein, was Lebensdauer, Besitz und mögliche Nullen betrifft. Wenn Sie beispielsweise devicean den Konstruktor von übergeben settingshaben, möchten Sie dennoch im aufrufenden Bereich oder nur über darauf verweisen können settings? Wenn letzteres unique_ptrnützlich ist. Haben Sie auch ein Szenario, in dem der Rückgabewert von getDevice()ist null. Wenn nicht, geben Sie einfach eine Referenz zurück.
Keith
2
Ja, a shared_ptrist in 8/10 Fällen korrekt. Die anderen 2/10 werden zwischen unique_ptrund aufgeteilt weak_ptr. Wird auch weak_ptrallgemein verwendet, um Zirkelverweise zu brechen; Ich bin nicht sicher, ob Ihre Verwendung als korrekt angesehen wird.
Collin Dauphinee
2
Welches Eigentum möchten Sie zunächst für das deviceDatenmitglied? Das muss man zuerst entscheiden.
Juanchopanza
1
Ok, ich verstehe, dass ich als Aufrufer unique_ptrstattdessen einen verwenden und den Besitz aufgeben könnte , wenn ich den Konstruktor aufrufe, wenn ich weiß , dass ich ihn vorerst nicht mehr brauche. Aber als Designer der SettingsKlasse weiß ich nicht, ob der Anrufer auch eine Referenz behalten möchte. Möglicherweise wird das Gerät an vielen Orten verwendet. Ok, vielleicht ist das genau dein Punkt. In diesem Fall wäre ich nicht der alleinige Eigentümer, und dann würde ich wohl shared_ptr verwenden. Und: Smart Points ersetzen also Zeiger, aber keine Referenzen, oder?
Michaelk
this-> device = device; Verwenden Sie auch Initialisierungslisten.
Nils

Antworten:

202

A unique_ptrwürde wegen nicht funktionieren getDevice(), oder?

Nein, nicht unbedingt. Hierbei ist es wichtig, die geeignete Eigentumsrichtlinie für Ihr DeviceObjekt zu bestimmen , dh wer der Eigentümer des Objekts sein wird, auf das Ihr (intelligenter) Zeiger zeigt.

Wird es nur die Instanz des SettingsObjekts sein ? Wird das DeviceObjekt zerstört werden müssen , automatisch , wenn das SettingsObjekt zerstört wird, oder sollte es das Objekt überleben?

Im ersten Fall std::unique_ptrbenötigen Sie Folgendes, da es Settingsden einzigen (eindeutigen) Eigentümer des spitzen Objekts und das einzige Objekt darstellt, das für dessen Zerstörung verantwortlich ist.

Unter dieser Annahme getDevice()sollte ein einfacher Beobachtungszeiger zurückgegeben werden (Beobachtungszeiger sind Zeiger, die das spitze Objekt nicht am Leben erhalten). Die einfachste Art, einen Zeiger zu beobachten, ist ein Rohzeiger:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ ANMERKUNG 1: Sie fragen sich vielleicht, warum ich hier Rohzeiger verwende, wenn alle immer wieder sagen, dass Rohzeiger schlecht, unsicher und gefährlich sind. Eigentlich ist das eine wertvolle Warnung, aber es ist wichtig, sie in den richtigen Kontext zu stellen: Rohzeiger sind schlecht, wenn sie für die manuelle Speicherverwaltung verwendet werden , dh für das Zuweisen und Freigeben von Objekten durch newund delete. Wenn es nur als Mittel verwendet wird, um eine Referenzsemantik zu erreichen und nicht besitzende, beobachtende Zeiger weiterzugeben, gibt es in rohen Zeigern nichts an sich Gefährliches, außer vielleicht der Tatsache, dass man darauf achten sollte, einen baumelnden Zeiger nicht zu dereferenzieren. - ENDE ANMERKUNG 1 ]

[ ANMERKUNG 2: Wie in den Kommentaren festgestellt wurde , könnte (und sollte) in diesem speziellen Fall, in dem der Besitz eindeutig ist und das besessene Objekt immer vorhanden sein wird (dh das interne Datenelement devicewird es niemals sein nullptr), funktionieren getDevice()(und sollte es vielleicht). Geben Sie eine Referenz anstelle eines Zeigers zurück. Obwohl dies zutrifft, habe ich beschlossen, hier einen Rohzeiger zurückzugeben, da dies eine kurze Antwort war, die man auf den Fall verallgemeinern könnte, in dem dies devicemöglich ist nullptr, und um zu zeigen, dass Rohzeiger in Ordnung sind, solange man sie nicht verwendet manuelle Speicherverwaltung. - ENDE ANMERKUNG 2 ]


Die Situation ist natürlich radikal anders, wenn Ihr SettingsObjekt nicht das ausschließliche Eigentum an dem Gerät haben sollte. Dies könnte beispielsweise der Fall sein, wenn die Zerstörung des SettingsObjekts nicht auch die Zerstörung des spitzen DeviceObjekts implizieren sollte .

Dies können nur Sie als Designer Ihres Programms sagen. Anhand des von Ihnen angegebenen Beispiels fällt es mir schwer zu sagen, ob dies der Fall ist oder nicht.

Um dies herauszufinden, können Sie sich fragen, ob es andere Objekte Settingsgibt, die berechtigt sind, das DeviceObjekt am Leben zu erhalten, solange sie einen Zeiger darauf halten, anstatt nur passive Beobachter zu sein. Wenn dies tatsächlich der Fall ist, benötigen Sie eine Richtlinie für gemeinsames Eigentum , die Folgendes std::shared_ptrbietet:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Beachten Sie, dass dies weak_ptrein Beobachtungszeiger ist , kein besitzender Zeiger - mit anderen Worten, er hält das spitze Objekt nicht am Leben, wenn alle anderen besitzenden Zeiger auf das spitze Objekt den Gültigkeitsbereich verlassen.

Der Vorteil weak_ptrgegenüber einem normalen Rohzeiger ist , dass man sicher sagen kann , ob weak_ptrsie baumelt oder nicht (dh ob er auf ein gültiges Objekt zeigt, oder wenn das Objekt ursprünglich zeigt auf zerstört worden ist ). Dies kann durch Aufrufen der expired()Member-Funktion für das weak_ptrObjekt erfolgen.

Andy Prowl
quelle
4
@LKK: Ja, richtig. A weak_ptrist immer eine Alternative zu rohen Beobachtungszeigern. In gewisser Hinsicht ist es sicherer, da Sie überprüfen können, ob es baumelt, bevor Sie es dereferenzieren, aber es ist auch mit einem gewissen Overhead verbunden. Wenn Sie leicht garantieren können, dass Sie einen baumelnden Zeiger nicht dereferenzieren, sollten Sie in der Lage sein, rohe Zeiger zu beobachten
Andy Prowl
6
Im ersten Fall wäre es wahrscheinlich sogar besser, getDevice()eine Referenz zurückgeben zu lassen , nicht wahr? Der Anrufer müsste also nicht nachsehen nullptr.
Objekt
5
@chico: Nicht sicher, was du meinst. auto myDevice = settings.getDevice()erstellt eine neue Instanz des Deviceaufgerufenen Typs myDeviceund kopiert sie aus der Instanz, auf die durch die zurückgegebene Referenz verwiesen wird getDevice(). Wenn Sie myDeviceeine Referenz sein möchten , müssen Sie dies tun auto& myDevice = settings.getDevice(). Wenn ich also nichts vermisse, sind wir wieder in der gleichen Situation, die wir ohne Verwendung hatten auto.
Andy Prowl
2
@Purrformance: Da Sie das Eigentum an dem Objekt nicht verraten möchten, unique_ptreröffnet die Übergabe eines modifizierbaren Objekts an einen Client die Möglichkeit, dass der Client von diesem Objekt abweicht , wodurch das Eigentum erworben wird und Sie einen Nullzeiger (eindeutigen Zeiger) erhalten.
Andy Prowl
7
@Purrformance: Während dies einen Kunden daran hindern würde, sich zu bewegen (es sei denn, der Kunde ist ein verrückter Wissenschaftler, der sich für const_casts interessiert ), würde ich es persönlich nicht tun. Es enthüllt ein Implementierungsdetail, dh die Tatsache, dass das Eigentum einzigartig ist und durch a realisiert wird unique_ptr. Ich sehe die Dinge so: Wenn Sie das Eigentum übergeben möchten, müssen Sie einen intelligenten Zeiger übergeben unique_ptroder zurückgeben ( oder shared_ptr, abhängig von der Art des Eigentums). Wenn Sie den Besitz nicht übergeben / zurückgeben möchten / müssen, verwenden Sie einen (ordnungsgemäß constqualifizierten) Zeiger oder eine Referenz, hauptsächlich abhängig davon, ob das Argument null sein kann oder nicht.
Andy Prowl
0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrwird nur für Referenzschleifen verwendet. Der Abhängigkeitsgraph muss ein azyklisch gerichteter Graph sein. In gemeinsam genutzten Zeigern gibt es 2 Referenzzählungen: 1 für shared_ptrs und 1 für alle Zeiger ( shared_ptrund weak_ptr). Wenn alle shared_ptrs entfernt sind, wird der Zeiger gelöscht. Wenn ein Zeiger von benötigt wird weak_ptr, locksollte verwendet werden, um den Zeiger zu erhalten, falls vorhanden.

Naszta
quelle
Wenn ich Ihre Antwort richtig verstehe, ersetzen intelligente Zeiger rohe Zeiger, aber nicht unbedingt Referenzen?
Michaelk
Gibt es tatsächlich zwei Referenzzählungen in a shared_ptr? Können Sie bitte erklären, warum? Soweit ich weak_ptrweiß , muss nicht gezählt werden, da shared_ptrbeim Bearbeiten des Objekts einfach ein neues erstellt wird (sofern das zugrunde liegende Objekt noch vorhanden ist).
Björn Pollex
@ BjörnPollex: Ich habe ein kurzes Beispiel für Sie erstellt: Link . Ich habe nicht alles implementiert, nur die Kopierkonstruktoren und lock. Die Boost- Version ist auch beim Referenzzählen threadsicher ( deletewird nur einmal aufgerufen).
Naszta
@Naszta: Ihr Beispiel zeigt, dass es möglich ist , dies mit zwei Referenzzählern zu implementieren, aber Ihre Antwort legt nahe, dass dies erforderlich ist , was ich nicht glaube. Könnten Sie dies bitte in Ihrer Antwort klarstellen?
Björn Pollex
1
@ BjörnPollex, um festzustellen, weak_ptr::lock()ob das Objekt abgelaufen ist, muss es den "Steuerblock" überprüfen, der den ersten Referenzzähler und Zeiger auf das Objekt enthält, damit der Steuerblock nicht zerstört wird, solange noch weak_ptrObjekte verwendet werden. weak_ptrDaher muss die Anzahl der Objekte verfolgt werden, was der zweite Referenzzähler tut. Das Objekt wird zerstört, wenn die erste Ref-Zählung auf Null fällt. Der Steuerblock wird zerstört, wenn die zweite Ref-Zählung auf Null fällt.
Jonathan Wakely