Was ist in der Praxis mit C ++ RAII , was sind intelligente Zeiger , wie werden diese in einem Programm implementiert und welche Vorteile bietet die Verwendung von RAII mit intelligenten Zeigern?
quelle
Was ist in der Praxis mit C ++ RAII , was sind intelligente Zeiger , wie werden diese in einem Programm implementiert und welche Vorteile bietet die Verwendung von RAII mit intelligenten Zeigern?
Ein einfaches (und möglicherweise überstrapaziertes) Beispiel für RAII ist eine Dateiklasse. Ohne RAII könnte der Code ungefähr so aussehen:
File file("/path/to/file");
// Do stuff with file
file.close();
Mit anderen Worten, wir müssen sicherstellen, dass wir die Datei schließen, sobald wir damit fertig sind. Dies hat zwei Nachteile: Erstens müssen wir, wo immer wir File verwenden, File :: close () aufrufen. Wenn wir dies vergessen, halten wir die Datei länger als nötig fest. Das zweite Problem ist, was passiert, wenn eine Ausnahme ausgelöst wird, bevor wir die Datei schließen?
Java löst das zweite Problem mit einer finally-Klausel:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
oder seit Java 7 eine Try-with-Resource-Anweisung:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ löst beide Probleme mit RAII, dh das Schließen der Datei im Destruktor von File. Solange das File-Objekt zum richtigen Zeitpunkt zerstört wird (was es auch sein sollte), wird das Schließen der Datei für uns erledigt. Unser Code sieht jetzt ungefähr so aus:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Dies ist in Java nicht möglich, da nicht garantiert wird, wann das Objekt zerstört wird. Daher können wir nicht garantieren, wann eine Ressource wie eine Datei freigegeben wird.
Auf intelligente Zeiger - häufig erstellen wir nur Objekte auf dem Stapel. Zum Beispiel (und ein Beispiel aus einer anderen Antwort stehlen):
void foo() {
std::string str;
// Do cool things to or using str
}
Das funktioniert gut - aber was ist, wenn wir str zurückgeben wollen? Wir könnten das schreiben:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Also, was ist daran falsch? Nun, der Rückgabetyp ist std :: string - das heißt, wir geben nach Wert zurück. Dies bedeutet, dass wir str kopieren und die Kopie tatsächlich zurückgeben. Dies kann teuer sein, und wir möchten möglicherweise die Kosten für das Kopieren vermeiden. Daher könnten wir auf die Idee kommen, als Referenz oder als Zeiger zurückzukehren.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
Leider funktioniert dieser Code nicht. Wir geben einen Zeiger auf str zurück - aber str wurde auf dem Stapel erstellt, sodass wir gelöscht werden, sobald wir foo () beenden. Mit anderen Worten, wenn der Anrufer den Zeiger erhält, ist er nutzlos (und wahrscheinlich schlimmer als nutzlos, da seine Verwendung alle möglichen funky Fehler verursachen kann).
Also, was ist die Lösung? Wir könnten str mit new auf dem Heap erstellen - auf diese Weise wird str nicht zerstört, wenn foo () abgeschlossen ist.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Natürlich ist diese Lösung auch nicht perfekt. Der Grund ist, dass wir str erstellt haben, es aber nie löschen. Dies ist in einem sehr kleinen Programm möglicherweise kein Problem, aber im Allgemeinen möchten wir sicherstellen, dass wir es löschen. Wir könnten einfach sagen, dass der Anrufer das Objekt löschen muss, wenn er damit fertig ist. Der Nachteil ist, dass der Anrufer den Speicher verwalten muss, was die Komplexität erhöht und möglicherweise zu Fehlern führt. Dies führt zu einem Speicherverlust, dh, dass ein Objekt nicht gelöscht wird, obwohl es nicht mehr benötigt wird.
Hier kommen intelligente Zeiger ins Spiel. Im folgenden Beispiel wird shared_ptr verwendet. Ich schlage vor, dass Sie sich die verschiedenen Arten von intelligenten Zeigern ansehen, um zu erfahren, was Sie tatsächlich verwenden möchten.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Jetzt zählt shared_ptr die Anzahl der Verweise auf str. Zum Beispiel
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Jetzt gibt es zwei Verweise auf dieselbe Zeichenfolge. Sobald keine Verweise mehr auf str vorhanden sind, wird es gelöscht. Als solches müssen Sie sich nicht mehr darum kümmern, es selbst zu löschen.
Schnelle Bearbeitung: Wie einige Kommentare bereits erwähnt haben, ist dieses Beispiel aus (zumindest!) Zwei Gründen nicht perfekt. Erstens ist das Kopieren einer Zeichenfolge aufgrund der Implementierung von Zeichenfolgen in der Regel kostengünstig. Zweitens ist die Rückgabe nach Wert aufgrund der so genannten Rückgabewertoptimierung möglicherweise nicht teuer, da der Compiler einige clevere Maßnahmen ergreifen kann, um die Dinge zu beschleunigen.
Versuchen wir also ein anderes Beispiel mit unserer File-Klasse.
Angenommen, wir möchten eine Datei als Protokoll verwenden. Dies bedeutet, dass wir unsere Datei nur im Anhänge-Modus öffnen möchten:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Lassen Sie uns nun unsere Datei als Protokoll für einige andere Objekte festlegen:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Leider endet dieses Beispiel schrecklich - die Datei wird geschlossen, sobald diese Methode endet, was bedeutet, dass foo und bar jetzt eine ungültige Protokolldatei haben. Wir könnten eine Datei auf dem Heap erstellen und einen Zeiger auf die Datei sowohl an foo als auch an bar übergeben:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Aber wer ist dann für das Löschen der Datei verantwortlich? Wenn keine Datei gelöscht wird, liegt sowohl ein Speicher- als auch ein Ressourcenverlust vor. Wir wissen nicht, ob foo oder bar zuerst mit der Datei fertig werden, daher können wir auch nicht erwarten, dass die Datei selbst gelöscht wird. Wenn beispielsweise foo die Datei löscht, bevor der Balken damit fertig ist, hat der Balken jetzt einen ungültigen Zeiger.
Wie Sie vielleicht erraten haben, könnten wir intelligente Zeiger verwenden, um uns zu helfen.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Jetzt muss sich niemand mehr um das Löschen von Dateien kümmern. Sobald sowohl foo als auch bar fertig sind und keine Verweise mehr auf die Datei vorhanden sind (wahrscheinlich aufgrund der Zerstörung von foo und bar), wird die Datei automatisch gelöscht.
RAII Dies ist ein seltsamer Name für ein einfaches, aber großartiges Konzept. Besser ist der Name Scope Bound Resource Management (SBRM). Die Idee ist, dass Sie häufig Ressourcen am Anfang eines Blocks zuweisen und diese am Ende eines Blocks freigeben müssen. Das Verlassen des Blocks kann durch normale Flusskontrolle, Herausspringen und sogar durch eine Ausnahme erfolgen. Um all diese Fälle abzudecken, wird der Code komplizierter und redundanter.
Nur ein Beispiel ohne SBRM:
Wie Sie sehen, gibt es viele Möglichkeiten, wie wir pwned werden können. Die Idee ist, dass wir das Ressourcenmanagement in einer Klasse zusammenfassen. Durch die Initialisierung des Objekts wird die Ressource erfasst ("Ressourcenerfassung ist Initialisierung"). Beim Verlassen des Blocks (Blockbereich) wird die Ressource wieder freigegeben.
Das ist schön, wenn Sie eigene Klassen haben, die nicht nur zum Zuweisen / Freigeben von Ressourcen dienen. Die Zuweisung wäre nur ein zusätzliches Anliegen, um ihre Arbeit zu erledigen. Sobald Sie jedoch nur Ressourcen zuweisen / freigeben möchten, wird das oben Genannte unhandlich. Sie müssen für jede Art von Ressource, die Sie erwerben, eine Wrapping-Klasse schreiben. Um dies zu vereinfachen, können Sie mit intelligenten Zeigern diesen Prozess automatisieren:
Normalerweise sind intelligente Zeiger dünne Wrapper für Neu / Löschen, die zufällig aufgerufen werden,
delete
wenn die Ressource, die sie besitzen, den Gültigkeitsbereich verlässt. Bei einigen intelligenten Zeigern wie shared_ptr können Sie ihnen einen sogenannten Deleter mitteilen, der anstelle von verwendet wirddelete
. Auf diese Weise können Sie beispielsweise Fensterhandles, Ressourcen für reguläre Ausdrücke und andere beliebige Elemente verwalten, solange Sie shared_ptr über den richtigen Deleter informieren.Es gibt verschiedene intelligente Zeiger für verschiedene Zwecke:
unique_ptr
ist ein intelligenter Zeiger, der ausschließlich ein Objekt besitzt. Es ist nicht in Boost, aber es wird wahrscheinlich im nächsten C ++ Standard erscheinen. Es ist nicht kopierbar , unterstützt jedoch die Übertragung des Eigentums . Ein Beispielcode (nächstes C ++):Code:
Im Gegensatz zu auto_ptr kann unique_ptr in einen Container eingefügt werden, da Container nicht kopierbare (aber bewegliche) Typen wie Streams und unique_ptr enthalten können.
scoped_ptr
ist ein Boost-Smart-Zeiger, der weder kopierbar noch beweglich ist. Es ist die perfekte Lösung, wenn Sie sicherstellen möchten, dass Zeiger gelöscht werden, wenn Sie den Gültigkeitsbereich verlassen.Code:
shared_ptr
ist für das gemeinsame Eigentum. Daher ist es sowohl kopierbar als auch beweglich. Mehrere Smart-Pointer-Instanzen können dieselbe Ressource besitzen. Sobald der letzte Smart Pointer, dem die Ressource gehört, den Gültigkeitsbereich verlässt, wird die Ressource freigegeben. Ein reales Beispiel für eines meiner Projekte:Code:
Wie Sie sehen, wird die Plotquelle (Funktion fx) gemeinsam genutzt, aber jeder hat einen eigenen Eintrag, auf den wir die Farbe einstellen. Es gibt eine schwache_ptr-Klasse, die verwendet wird, wenn Code auf die Ressource verweisen muss, die einem intelligenten Zeiger gehört, die Ressource jedoch nicht besitzen muss. Anstatt einen rohen Zeiger zu übergeben, sollten Sie dann ein schwaches_ptr erstellen. Es wird eine Ausnahme ausgelöst, wenn festgestellt wird, dass Sie versuchen, über einen schwachen_PTR-Zugriffspfad auf die Ressource zuzugreifen, obwohl kein Shared_PTR mehr die Ressource besitzt.
quelle
unique_ptr
, undsort
sie werden ebenfalls geändert.Die Prämisse und die Gründe sind im Konzept einfach.
RAII ist das Entwurfsparadigma, um sicherzustellen, dass Variablen alle erforderlichen Initialisierungen in ihren Konstruktoren und alle erforderlichen Bereinigungen in ihren Destruktoren verarbeiten. Dies reduziert alle Initialisierungen und Bereinigungen auf einen einzigen Schritt.
C ++ erfordert kein RAII, aber es wird zunehmend akzeptiert, dass die Verwendung von RAII-Methoden robusteren Code erzeugt.
Der Grund, warum RAII in C ++ nützlich ist, besteht darin, dass C ++ die Erstellung und Zerstörung von Variablen beim Betreten und Verlassen des Bereichs intrinsisch verwaltet, sei es durch normalen Codefluss oder durch durch eine Ausnahme ausgelöstes Abwickeln des Stapels. Das ist ein Werbegeschenk in C ++.
Durch die Verknüpfung aller Initialisierungen und Bereinigungen mit diesen Mechanismen wird sichergestellt, dass C ++ diese Arbeit auch für Sie erledigt.
Das Sprechen über RAII in C ++ führt normalerweise zur Diskussion intelligenter Zeiger, da Zeiger bei der Bereinigung besonders anfällig sind. Bei der Verwaltung von Heap-zugewiesenem Speicher, der von malloc oder new erworben wurde, liegt es normalerweise in der Verantwortung des Programmierers, diesen Speicher freizugeben oder zu löschen, bevor der Zeiger zerstört wird. Intelligente Zeiger verwenden die RAII-Philosophie, um sicherzustellen, dass Heap-zugewiesene Objekte jedes Mal zerstört werden, wenn die Zeigervariable zerstört wird.
quelle
Smart Pointer ist eine Variation von RAII. RAII bedeutet, dass die Ressourcenbeschaffung eine Initialisierung ist. Smart Pointer erfasst eine Ressource (Speicher) vor der Verwendung und wirft sie dann automatisch in einem Destruktor weg. Zwei Dinge passieren:
Ein weiteres Beispiel ist beispielsweise der Netzwerk-Socket RAII. In diesem Fall:
Wie Sie sehen, ist RAII in den meisten Fällen ein sehr nützliches Werkzeug, da es den Menschen hilft, sich zu legen.
C ++ - Quellen für intelligente Zeiger sind in Millionenhöhe im Internet verfügbar, einschließlich der Antworten über mir.
quelle
Boost hat eine Reihe von diesen, einschließlich der in Boost.Interprocess für Shared Memory. Dies vereinfacht die Speicherverwaltung erheblich, insbesondere in kopfschmerzauslösenden Situationen, in denen 5 Prozesse dieselbe Datenstruktur verwenden: Wenn alle mit einem Speicherblock fertig sind, möchten Sie, dass dieser automatisch freigegeben wird und nicht dort sitzen muss, um dies herauszufinden Wer sollte für das Aufrufen
delete
eines Speicherblocks verantwortlich sein, damit Sie nicht mit einem Speicherverlust oder einem Zeiger enden, der fälschlicherweise zweimal freigegeben wird und den gesamten Heap beschädigen kann?quelle
Unabhängig davon, was passiert, wird der Balken ordnungsgemäß gelöscht, sobald der Bereich der Funktion foo () verlassen wurde.
Interne std :: string-Implementierungen verwenden häufig Zeiger mit Referenzzählung. Die interne Zeichenfolge muss also nur kopiert werden, wenn sich eine der Kopien der Zeichenfolgen geändert hat. Ein intelligenter Zeiger mit Referenzzählung ermöglicht es daher, nur bei Bedarf etwas zu kopieren.
Darüber hinaus ermöglicht die interne Referenzzählung, dass der Speicher ordnungsgemäß gelöscht wird, wenn die Kopie der internen Zeichenfolge nicht mehr benötigt wird.
quelle