Was ist unter Ressourcenakquisition unter Initialisierung (RAII) zu verstehen?

Antworten:

374

Es ist ein wirklich schrecklicher Name für ein unglaublich leistungsfähiges Konzept und vielleicht eines der wichtigsten Dinge, die C ++ - Entwickler vermissen, wenn sie in andere Sprachen wechseln. Es gab eine gewisse Bewegung, um zu versuchen, dieses Konzept in Scope-Bound Resource Management umzubenennen , obwohl es sich noch nicht durchgesetzt zu haben scheint.

Wenn wir "Ressource" sagen, meinen wir nicht nur Speicher - es können Datei-Handles, Netzwerk-Sockets, Datenbank-Handles, GDI-Objekte sein ... Kurz gesagt, Dinge, die wir endlich zur Verfügung haben und die wir müssen kontrollieren ihre Verwendung. Der Aspekt "Gültigkeitsbereich gebunden" bedeutet, dass die Lebensdauer des Objekts an den Gültigkeitsbereich einer Variablen gebunden ist. Wenn die Variable den Gültigkeitsbereich verlässt, gibt der Destruktor die Ressource frei. Eine sehr nützliche Eigenschaft davon ist, dass es für eine größere Ausnahmesicherheit sorgt. Vergleichen Sie zum Beispiel Folgendes:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

Mit dem RAII

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

In diesem letzteren Fall werden die lokalen Variablen zerstört, wenn die Ausnahme ausgelöst und der Stapel abgewickelt wird, wodurch sichergestellt wird, dass unsere Ressource bereinigt wird und nicht ausläuft.

the_mandrill
quelle
2
@the_mandrill: Ich habe dieses Programm mit ideone.com/1Jjzuc ausprobiert. Es gibt jedoch keinen Destruktoraufruf. Laut tomdalling.com/blog/software-design/… garantiert C ++, dass der Destruktor von Objekten auf dem Stapel aufgerufen wird, selbst wenn eine Ausnahme ausgelöst wird. Warum wurde der Destruktor hier nicht ausgeführt? Ist meine Ressource durchgesickert oder wird sie niemals freigegeben oder freigegeben?
Zerstörer
8
Eine Ausnahme wird ausgelöst, aber Sie fangen sie nicht ab, sodass die Anwendung beendet wird. Wenn Sie mit einem Versuch {} catch () {} abschließen,
the_mandrill
2
Ich bin mir nicht ganz sicher, ob dies Scope-Bounddie beste Namenswahl ist, da Speicherklassenspezifizierer zusammen mit dem Bereich die Speicherdauer einer Entität bestimmen. Es ist vielleicht eine nützliche Vereinfachung, es auf den Umfang zu beschränken, aber es ist nicht 100% genau
SebNag
125

Dies ist eine Programmiersprache, die kurz bedeutet, dass Sie

  • Kapselung einer Ressource in eine Klasse (deren Konstruktor normalerweise - aber nicht unbedingt ** - die Ressource abruft und deren Destruktor sie immer freigibt)
  • Verwenden Sie die Ressource über eine lokale Instanz der Klasse *
  • Die Ressource wird automatisch freigegeben, wenn das Objekt den Gültigkeitsbereich verlässt

Dies garantiert, dass alles, was passiert, während die Ressource verwendet wird, irgendwann freigegeben wird (sei es aufgrund einer normalen Rückgabe, einer Zerstörung des enthaltenen Objekts oder einer ausgelösten Ausnahme).

Es ist eine weit verbreitete bewährte Methode in C ++, da es nicht nur ein sicherer Weg ist, mit Ressourcen umzugehen, sondern auch Ihren Code viel sauberer macht, da Sie keinen Fehlerbehandlungscode mit der Hauptfunktionalität mischen müssen.

* Update: "lokal" kann eine lokale Variable oder eine nicht statische Mitgliedsvariable einer Klasse bedeuten. Im letzteren Fall wird die Mitgliedsvariable mit ihrem Eigentümerobjekt initialisiert und zerstört.

** Update2: Wie @sbi betonte, kann die Ressource - obwohl sie häufig innerhalb des Konstruktors zugewiesen wird - auch außerhalb zugewiesen und als Parameter übergeben werden.

Péter Török
quelle
1
AFAIK, das Akronym bedeutet nicht, dass sich das Objekt auf einer lokalen (Stapel-) Variablen befinden muss. Es kann sich um eine Mitgliedsvariable eines anderen Objekts handeln. Wenn also das 'Holding'-Objekt zerstört wird, wird auch das Mitgliedsobjekt zerstört und die Ressource freigegeben. Tatsächlich denke ich, dass das Akronym speziell nur bedeutet, dass es keine open()/ close()Methoden gibt, um die Ressource zu initialisieren und freizugeben, nur den Konstruktor und den Destruktor, so dass das "Halten" der Ressource nur die Lebensdauer des Objekts ist, egal ob diese Lebensdauer ist behandelt durch den Kontext (Stapel) oder explizit (dynamische Zuordnung)
Javier
1
Eigentlich sagt nichts, dass die Ressource im Konstruktor erfasst werden muss. Datei - Streams, Strings einen anderer Behälter tun, aber die Ressource könnte genauso gut wird weitergegeben an den Konstruktor, wie es normalerweise der Fall mit Smart - Pointer. Da Ihre Antwort die am besten bewertete ist, möchten Sie dies möglicherweise beheben.
sbi
Es ist kein Akronym, es ist eine Abkürzung. IIRC die meisten Leute sprechen es "ar ey ay ay" aus, so dass es sich nicht wirklich für ein Akronym wie say DARPA qualifiziert, das DARPA statt Dinkel ausgesprochen wird. Außerdem würde ich sagen, dass RAII eher ein Paradigma als eine bloße Redewendung ist.
dtech
@ Peter Torok: Ich habe dieses Programm mit ideone.com/1Jjzuc ausprobiert . Es gibt jedoch keinen Destruktoraufruf. Die tomdalling.com/blog/software-design/... sagt , dass C ++ garantiert , dass der destructor von Objekten auf dem Stapel aufgerufen werden, auch wenn eine Ausnahme ausgelöst wird. Warum wurde der Destruktor hier nicht ausgeführt? Ist meine Ressource durchgesickert oder wird sie niemals freigegeben oder freigegeben?
Zerstörer
50

„RAH“ steht für „Ressourcenerfassung ist Initialisierung“ und ist eigentlich recht eine falsche Bezeichnung, da es nicht Ressource Erwerb (und die Initialisierung eines Objekts) mit betroffen ist, aber die Freigabe der Ressource (durch Zerstörung eines Objekts ).
Aber RAII ist der Name, den wir haben und der bleibt.

Das Kernstück der Redewendung besteht darin, Ressourcen (Speicherblöcke, geöffnete Dateien, entsperrte Mutexe, Sie-Name-it) in lokalen, automatischen Objekten zu kapseln und den Destruktor dieses Objekts die Ressource freizugeben, wenn das Objekt am zerstört wird Ende des Bereichs, zu dem es gehört:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

Natürlich sind Objekte nicht immer lokale, automatische Objekte. Sie könnten auch Mitglieder einer Klasse sein:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

Wenn solche Objekte den Speicher verwalten, werden sie häufig als "intelligente Zeiger" bezeichnet.

Es gibt viele Variationen davon. In den ersten Codefragmenten stellt sich beispielsweise die Frage, was passieren würde, wenn jemand kopieren wollte obj. Der einfachste Ausweg wäre, das Kopieren einfach zu verbieten. std::unique_ptr<>Dies ist ein intelligenter Zeiger, der Teil der Standardbibliothek ist, wie sie im nächsten C ++ - Standard enthalten ist.
Ein weiterer solcher intelligenter Zeiger std::shared_ptrbietet "gemeinsames Eigentum" an der Ressource (einem dynamisch zugewiesenen Objekt), die er enthält. Das heißt, es kann frei kopiert werden und alle Kopien beziehen sich auf dasselbe Objekt. Der Smart Pointer verfolgt, wie viele Kopien auf dasselbe Objekt verweisen, und löscht es, wenn das letzte zerstört wird.
Eine dritte Variante wird von vorgestelltstd::auto_ptr Dies implementiert eine Art Verschiebungssemantik: Ein Objekt gehört nur einem Zeiger, und der Versuch, ein Objekt zu kopieren, führt (durch Syntax-Hackery) dazu, dass das Eigentum an dem Objekt auf das Ziel des Kopiervorgangs übertragen wird.

sbi
quelle
4
std::auto_ptrist veraltete Version von std::unique_ptr. std::auto_ptrArt der simulierten Verschiebungssemantik, so weit es in C ++ 98 möglich war, std::unique_ptrverwendet die neue Verschiebungssemantik von C ++ 11. Eine neue Klasse wurde erstellt, da die Verschiebungssemantik von C ++ 11 expliziter ist ( std::moveaußer temporär erforderlich ), während sie für jede Kopie von Nicht-Konstanten standardmäßig verwendet wurde std::auto_ptr.
Jan Hudec
@JiahaoCai: Vor vielen Jahren (im Usenet) hat Stroustrup dies selbst gesagt.
sbi
21

Die Lebensdauer eines Objekts wird durch seinen Umfang bestimmt. Manchmal müssen oder müssen wir jedoch ein Objekt erstellen, das unabhängig von dem Bereich lebt, in dem es erstellt wurde. In C ++ wird der Operator newverwendet, um ein solches Objekt zu erstellen. Und um das Objekt zu zerstören, kann der Bediener deleteverwendet werden. Vom Bediener erstellte Objekte newwerden dynamisch zugewiesen, dh im dynamischen Speicher zugewiesen (auch als Heap oder freier Speicher bezeichnet ). Ein Objekt, das von erstellt wurde, newbleibt also bestehen, bis es mit explizit zerstört wird delete.

Einige Fehler, die bei der Verwendung auftreten können newund deletesind:

  • Durchgesickertes Objekt (oder Speicher): Verwenden Sie newdiese Option, um ein Objekt zuzuweisen und deletedas Objekt zu vergessen .
  • Vorzeitiges Löschen (oder baumelnde Referenz ): Halten Sie einen anderen Zeiger auf ein Objekt, deletedas Objekt, und verwenden Sie dann den anderen Zeiger.
  • Doppeltes Löschen : Es deletewird zweimal versucht, ein Objekt zu löschen .

Im Allgemeinen werden Bereichsvariablen bevorzugt. RAII kann jedoch als Alternative zu newund verwendet werden delete, um ein Objekt unabhängig von seinem Umfang zum Leben zu erwecken. Eine solche Technik besteht darin, den Zeiger auf das Objekt zu nehmen, das auf dem Heap zugewiesen wurde, und ihn in einem Handle / Manager-Objekt zu platzieren . Letzterer hat einen Destruktor, der sich um die Zerstörung des Objekts kümmert. Dadurch wird sichergestellt, dass das Objekt für alle Funktionen verfügbar ist, die Zugriff darauf wünschen, und dass das Objekt zerstört wird, wenn die Lebensdauer des Handle-Objekts endet, ohne dass eine explizite Bereinigung erforderlich ist.

Beispiele aus der C ++ - Standardbibliothek, die RAII verwenden, sind std::stringund std::vector.

Betrachten Sie diesen Code:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

Wenn Sie einen Vektor erstellen und Elemente darauf verschieben, ist es Ihnen egal, ob Sie solche Elemente zuweisen oder freigeben. Der Vektor verwendet new, um Speicherplatz für seine Elemente auf dem Heap zuzuweisen und deletediesen Speicherplatz freizugeben. Als Benutzer von vector interessieren Sie sich nicht für die Implementierungsdetails und vertrauen darauf, dass vector nicht ausläuft. In diesem Fall ist der Vektor das Handle-Objekt seiner Elemente.

Weitere Beispiele aus der Standardbibliothek , dass die Verwendung RAH sind std::shared_ptr, std::unique_ptrund std::lock_guard.

Ein anderer Name für diese Technik ist SBRM , kurz für Scope-Bound Resource Management .

Elmiomar
quelle
1
"SBRM" macht für mich viel mehr Sinn. Ich kam zu dieser Frage, weil ich dachte, ich verstehe RAII, aber der Name warf mich ab. Als ich hörte, dass es stattdessen als "Scope-Bound Resource Management" beschrieben wurde, wurde mir sofort klar, dass ich das Konzept tatsächlich verstand.
JShorthouse
Ich bin mir nicht sicher, warum dies nicht als Antwort auf die Frage markiert wurde. Es ist eine sehr gründliche und gut geschriebene Antwort, danke @elmiomar
Abdelrahman Shoman
13

Das Buch C ++ Programming with Design Patterns Revealed beschreibt RAII wie folgt:

  1. Alle Ressourcen erwerben
  2. Ressourcen nutzen
  3. Ressourcen freigeben

Wo

  • Ressourcen werden als Klassen implementiert, und alle Zeiger sind von Klassenumbrüchen umgeben (was sie zu intelligenten Zeigern macht).

  • Ressourcen werden durch Aufrufen ihrer Konstruktoren erfasst und implizit (in umgekehrter Reihenfolge des Erwerbs) durch Aufrufen ihrer Destruktoren freigegeben.

Dennis
quelle
1
@Brandin Ich habe meinen Beitrag so bearbeitet, dass sich die Leser auf den Inhalt konzentrieren, der wichtig ist, anstatt über die Grauzone des Urheberrechts zu diskutieren, was eine faire Verwendung darstellt.
Dennis
7

Eine RAII-Klasse besteht aus drei Teilen:

  1. Die Ressource wird im Destruktor aufgegeben
  2. Instanzen der Klasse werden stapelweise zugewiesen
  3. Die Ressource wird im Konstruktor erfasst. Dieser Teil ist optional, aber üblich.

RAII steht für "Resource Acquisition is Initialization". Im Teil "Ressourcenbeschaffung" von RAII beginnen Sie etwas, das später beendet werden muss, z.

  1. Datei öffnen
  2. Speicher zuweisen
  3. Ein Schloss erwerben

Der Teil "Ist Initialisierung" bedeutet, dass die Erfassung innerhalb des Konstruktors einer Klasse erfolgt.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/

Mohammad Moridi
quelle
5

Die manuelle Speicherverwaltung ist ein Albtraum, den Programmierer seit der Erfindung des Compilers erfunden haben. Programmiersprachen mit Garbage Collectors erleichtern das Leben, jedoch auf Kosten der Leistung. In diesem Artikel - Beseitigung des Müllsammlers : Der RAII-Weg gibt uns der Toptal-Ingenieur Peter Goodspeed-Niklaus einen Einblick in die Geschichte der Müllsammler und erklärt, wie Vorstellungen von Eigentum und Ausleihe dazu beitragen können, Müllsammler zu beseitigen, ohne ihre Sicherheitsgarantien zu beeinträchtigen.

Dmitry Pavlov
quelle