RAII und Smart Pointer in C ++

193

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?

Rob Kam
quelle

Antworten:

317

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.

Michael Williamson
quelle
7
Es sollte beachtet werden, dass viele Zeichenfolgenimplementierungen in Bezug auf einen Referenzzählzeiger implementiert sind. Diese Semantik beim Kopieren beim Schreiben macht die Rückgabe einer Zeichenfolge nach Wert wirklich kostengünstig.
7
Selbst für diejenigen, die dies nicht sind, implementieren viele Compiler eine NRV-Optimierung, die den Overhead übernehmen würde. Im Allgemeinen finde ich shared_ptr selten nützlich - bleiben Sie einfach bei RAII und vermeiden Sie Shared Ownership.
Nemanja Trifunovic
27
Das Zurückgeben eines Strings ist kein guter Grund, um wirklich intelligente Zeiger zu verwenden. Die Rückgabewertoptimierung kann die Rückgabe leicht optimieren, und die C ++ 1x-Verschiebungssemantik eliminiert eine Kopie vollständig (bei korrekter Verwendung). Zeigen Sie stattdessen ein Beispiel aus der realen Welt (zum Beispiel, wenn wir dieselbe Ressource verwenden) :)
Johannes Schaub - litb
1
Ich denke, Ihre frühzeitige Schlussfolgerung, warum Java dies nicht kann, ist nicht klar genug. Die einfachste Möglichkeit, diese Einschränkung in Java oder C # zu beschreiben, besteht darin, dass keine Zuordnung auf dem Stapel möglich ist. C # ermöglicht die Stapelzuweisung über ein spezielles Schlüsselwort. Sie verlieren jedoch die Typensicherheit.
ApplePieIsGood
4
@Nemanja Trifunovic: Mit RAII meinen Sie in diesem Zusammenhang das Zurückgeben von Kopien / das Erstellen von Objekten auf dem Stapel? Dies funktioniert nicht, wenn Sie Objekte von Typen zurückgeben / akzeptieren, die in Unterklassen unterteilt werden können. Dann müssen Sie einen Zeiger verwenden, um das Schneiden des Objekts zu vermeiden, und ich würde argumentieren, dass ein intelligenter Zeiger in diesen Fällen oft besser ist als ein roher.
Frank Osterfeld
141

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:

void o_really() {
     resource * r = allocate_resource();
     try {
         // something, which could throw. ...
     } catch(...) {
         deallocate_resource(r);
         throw;
     }
     if(...) { return; } // oops, forgot to deallocate
     deallocate_resource(r);
}

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.

struct resource_holder {
    resource_holder() {
        r = allocate_resource();
    }
    ~resource_holder() {
        deallocate_resource(r);
    }
    resource * r;
};

void o_really() {
     resource_holder r;
     // something, which could throw. ...
     if(...) { return; }
}

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:

shared_ptr<Entry> create_entry(Parameters p) {
    shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
    return e;
}

Normalerweise sind intelligente Zeiger dünne Wrapper für Neu / Löschen, die zufällig aufgerufen werden, deletewenn 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 wird delete. 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:

unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u

vector<unique_ptr<plot_src>> pv; 
pv.emplace_back(new plot_src); 
pv.emplace_back(new plot_src);

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:

void do_something() {
    scoped_ptr<pipe> sp(new pipe);
    // do something here...
} // when going out of scope, sp will delete the pointer automatically. 

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:

shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and 
// plot2 both still have references. 

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.

Johannes Schaub - litb
quelle
Soweit ich weiß, sind nicht kopierbare Objekte in STL-Containern überhaupt nicht gut zu verwenden, da sie auf Wertesemantik beruhen. Was passiert, wenn Sie diesen Container sortieren möchten? sort kopiert Elemente ...
fmuecke
C ++ 0x-Container werden so geändert, dass nur verschiebbare Typen wie berücksichtigt werden unique_ptr, und sortsie werden ebenfalls geändert.
Johannes Schaub - Litb
Erinnerst du dich, wo du den Begriff SBRM zum ersten Mal gehört hast? James versucht es aufzuspüren.
GManNickG
Welche Header oder Bibliotheken sollte ich einschließen, um diese zu verwenden? Weitere Lesungen dazu?
AtoMerz
Ein Ratschlag hier: Wenn es eine Antwort auf eine C ++ - Frage von @litb gibt, ist dies die richtige Antwort (unabhängig von den Stimmen oder der Antwort, die als "richtig" gekennzeichnet ist) ...
fnl
32

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.

Drew Dormann
quelle
Darüber hinaus - Zeiger sind die häufigste Anwendung von RAII - werden Sie wahrscheinlich tausende Male mehr Zeiger zuweisen als jede andere Ressource.
Eclipse
8

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:

  1. Wir weisen Speicher immer zu, bevor wir ihn verwenden, auch wenn wir keine Lust dazu haben - es ist schwierig, mit einem intelligenten Zeiger einen anderen Weg zu gehen. Wenn dies nicht passiert ist, werden Sie versuchen, auf den NULL-Speicher zuzugreifen, was zu einem Absturz führt (sehr schmerzhaft).
  2. Wir geben Speicher frei, auch wenn ein Fehler vorliegt. Es bleibt keine Erinnerung hängen.

Ein weiteres Beispiel ist beispielsweise der Netzwerk-Socket RAII. In diesem Fall:

  1. Wir öffnen den Netzwerk-Socket immer, bevor wir ihn verwenden, auch wenn wir keine Lust haben - mit RAII ist es schwierig, es anders zu machen. Wenn Sie dies ohne RAII versuchen, können Sie einen leeren Socket für beispielsweise eine MSN-Verbindung öffnen. Dann wird eine Nachricht wie "Lass es uns heute Abend tun" möglicherweise nicht übertragen, Benutzer werden nicht gelegt und Sie riskieren möglicherweise, gefeuert zu werden.
  2. Wir schließen den Netzwerk-Socket auch dann, wenn ein Fehler auftritt. Es bleibt kein Socket hängen, da dies verhindern könnte, dass die Antwortnachricht "Sicher unten" den Absender zurückschlägt.

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.

Mannicken
quelle
2

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 deleteeines 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?

Jason S.
quelle
0
void foo ()
{
   std :: string bar;
   // //
   // mehr Code hier
   // //
}}

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
1
void f () {Obj x; } Obj x wird durch Erstellen / Zerstören von Stapelrahmen (Abwickeln) gelöscht ... es hängt nicht mit der Ref-Zählung zusammen.
Hernán
Die Referenzzählung ist ein Merkmal der internen Implementierung der Zeichenfolge. RAII ist das Konzept hinter dem Löschen von Objekten, wenn das Objekt den Gültigkeitsbereich verlässt. Die Frage betraf RAII und auch intelligente Zeiger.
1
"Egal was passiert" - was passiert, wenn eine Ausnahme ausgelöst wird, bevor die Funktion zurückkehrt?
titaniumdecoy
Welche Funktion wird zurückgegeben? Wenn in foo eine Ausnahme ausgelöst wird, wird die Leiste gelöscht. Der Standardkonstruktor für das Auslösen einer Ausnahme wäre ein außergewöhnliches Ereignis.