RAII



  • hallo zusammen,

    in letzter zeit sehe ich immer wieder bei solchen code fragmenten:

    // some code
    fstream f;
    // 3 zeilen mit sonstwas
    f.open("test.txt", ios::in | ios::out);
    // ... code
    f << "Hallo Test" << endl;
    // ... code bis zum ende
    

    dass direkt leute hier im forum ankommen mit: "kennst du kein RAII?", "benutz doch mal RAII!", "du solltest dich mal nach RAII umschauen..."

    warum?
    was ist so toll daran und warum muss man das alles so machen?

    RAII heisst doch ressource acquisition is initialization was auf deutsch soviel heisst wie ressourcen anschaffung ist initialisierung. sprich wenn man ein objekt erstellt wird es auch direkt soweit initialisiert, dass es auch benutzt werden kann (das verstehe ich doch richtig oder?!?)

    gleichzeitig gehört zu diesem idiom, dass die angeschafften ressourcen beim zerstören des objektes auch automatisch freigegeben werden (alles andere wäre ja auch irgendwo dämlich o_O). und auch logisch ist, dass die STL/Boost-Bibliotheken dieses Idiom auch ziemlich ausreizen, was ja nicht mehr als richtig ist.

    Aber wieso ist es dann so, dass fstream zum beispiel noch open bereitstellt?
    Ok, die frage kann ich mir selbst beantworten: muss man den stream in einer klasse erstellen, weiss aber noch nicht welche datei genutzt wird, muss das nachträglich möglich sein z.B. ...
    Aber genau an dem punkt könnte man wiederum anders argumentieren, dass ein gerade am erstellendes objekt soweit initialisiert ist (oder halt auf dem weg dahin), dass es seinen internen filestream auch initialisieren kann. es wird ja auch RAII angewendet... (hoffe ihr versteht das >.>)

    und wieso ist es schlimm wen man, wie oben in dem ersten codebeispiel, sein objekt erst etwas später richtig initialisiert?
    intern wird im konstruktor doch auch die open methode genutzt, oder etwa nicht?
    jedenfalls würde ich solche klassen so designen, sonst schreibt man ja schön redundanten code...

    der grösste vorteil von RAII ist doch das automatisierte freigeben der ressourcen. aber das passiert doch auch ohne, dass man sein objekt im konstruktor vollständig initialisiert... (ansonsten müsste man ja noch mindestens eine boolsche variable hinzufügen, die angibt ob ein obejkt per methode oder per konstruktor initialisiert worden ist... -> schwachsinn)

    also wieso wird direkt mit steinen geworfen wenn einer nicht im konstruktor initialisiert?

    mfg mosho

    PS: ein paar zitate:

    xXx schrieb:

    xXx schrieb:

    Table::Table()
    {
        ifstream inputFile; // open könnte auch hier geschehen
    }
    

    Schön wenn du es weißt, aber wieso verwendest du es nicht?

    xXx schrieb:

    xXx schrieb:

    Gibt es denn einen Grund, außer dass man es gleich im Konstruktor mitgeben kann?

    (Außer Code-Reduktion, weniger Befehle in der Abfolge etc.)

    Bist du so geil darauf, eine Zeile mehr zu schreiben oder vermeidest du RAII generell?



  • Du hast RAII nicht verstanden. Das Objekt soll im Konstruktor vollständig initialisiert werden. Und wozu eine Zeile mehr schreiben, macht das Spaß?
    Hier geht es nicht nur um das, auch um die Lesbarkeit. Wenn jemand explizit öffnet und schließt, suche ich erst mal einen Grund dafür.

    Es ist so wie mit goto, man kann es in ganz (ganz ganz) wenigen Fällen brauchen, aber im Allgemeinen Fall ist es unnötig, führt zu Verwirrung und widerspricht RAII.

    Das ist ungefähr so, als würdest du rohen Speicher anfordern und dann mit placement-new später Initialisieren. Das ist schwachsinnig und führt zu Fehlern.
    Du kannst das open ja schließlich auch vergessen.



  • 314159265358979 schrieb:

    Du hast RAII nicht verstanden. Das Objekt soll im Konstruktor vollständig initialisiert werden. Und wozu eine Zeile mehr schreiben, macht das Spaß?

    hab ich das nicht verstanden?

    naja, ich finde open recht gut, da man sieht was passiert. wenn ich f.open(filename); sehe, dann weiss ich sofort: der öffnet eine datei
    bei fstream f(filename); ist das auch noch gut sichtbar
    aber irgendwann hört das auf...



  • Vollständig bedeutet, dass die File bereits benutzbar sein soll. (Oder allgemeiner, keine Initialisierung mehr vorgenommen werden muss)



  • Generell hat es 2 Gründe, statt open lieber den Konstruktor zu verwenden. Erstens spart man eine Zeile und Zweitens kommt man nicht in Gefahr, ein nicht vollständig initialisiertes Objekt zu verwenden. Erstens mag unwichtig sein, aber Zweitens tritt häufiger auf als man denkt.

    Wenn du z.B. mal mit C# oder Java programmiert hast, wirst du auf einige Stellen stoßen, wo du Objekte auf null prüfen musst, weil sie möglicherweise noch nicht initialisiert sind. In C++ gibt es sowas nicht. Jedes Objekt wird garantiert im Konstruktor initialisiert.

    RAII geht jetzt noch einen Schritt weiter. Im konkreten Fall ist garantiert, dass der Stream geöffnet ist. Entweder das, oder er wird gar nicht erstellt (Exception). Das Fehlerpotential wird also noch weiter verringert. Und warum sollte man das nicht nutzen?

    Dass trotzdem open und close angeboten wird, ist zu Gunsten der Fexibilität. Es gibt schon Anwendungen, wo ein explizites Öffnen und Schließen des Streams gewünscht ist, man wollte den Programmierer also nicht einschränken, nur sind diese selten und sollten in jedem Fall überdacht werden (weil in solchen Situationen das erwähnte Fehlerpotential wieder entsteht).



  • ipsec schrieb:

    Wenn du z.B. mal mit C# oder Java programmiert hast, wirst du auf einige Stellen stoßen, wo du Objekte auf null prüfen musst, weil sie möglicherweise noch nicht initialisiert sind. In C++ gibt es sowas nicht. Jedes Objekt wird garantiert im Konstruktor initialisiert.

    Dass trotzdem open und close angeboten wird, ist zu Gunsten der Fexibilität. Es gibt schon Anwendungen, wo ein explizites Öffnen und Schließen des Streams gewünscht ist, man wollte den Programmierer also nicht einschränken, nur sind diese selten und sollten in jedem Fall überdacht werden (weil in solchen Situationen das erwähnte Fehlerpotential wieder entsteht).

    zum ersten: ja im studium wird java gelehrt und danke. das ist mal einr ichtig gutes argument dem ich auch voll zustimmen würde...

    zum zweiten: also kann ich in meinen eigenen klassen ruhig initialisierungs methoden anbieten, aber auch neben einem konstruktor der diese itnern aufruft und damit das objekt schon bereit zur arbeit macht?
    (stimmt der technische aspekt denn, das open im konstruktor von fstream aufgerufen wird?)

    wie sieht das denn analog dazu mit deinitialierungs methoden aus z.b. exit oder fstream::close(); ?



  • ipsec schrieb:

    RAII geht jetzt noch einen Schritt weiter. Im konkreten Fall ist garantiert, dass der Stream geöffnet ist. Entweder das, oder er wird gar nicht erstellt (Exception).

    http://cplusplus.com/reference/iostream/fstream/fstream/
    sagt dazu:
    If the constructor is not successful in opening the file, the object is still created although no file is associated to the stream buffer and the stream's failbit is set (which can be checked with inherited member fail).

    Außerdem:
    Constructs an object of the fstream class. This implies the initialization of the associated filebuf object and the call to the constructor of its base class with the filebuf object as parameter.

    und:
    Additionally, when the second constructor version is used, the stream is associated with a physical file as if a call to the member function open with the same parameters was made.

    Die Frage ist einfach, ist das Objekt nicht auch initialisiert, wenn ich einfach nur den Standardkonstruktor benutze.
    Es ist schließlich nutzbar. Und an der Ressourcenfreigabe bei Verlassen des Scopes ändert sich nichts.



  • Skym0sh0 schrieb:

    (stimmt der technische aspekt denn, das open im konstruktor von fstream aufgerufen wird?)

    Vermutlich ja (zumindest würde ich es so implementieren).

    Belli schrieb:

    ipsec schrieb:

    RAII geht jetzt noch einen Schritt weiter. Im konkreten Fall ist garantiert, dass der Stream geöffnet ist. Entweder das, oder er wird gar nicht erstellt (Exception).

    http://cplusplus.com/reference/iostream/fstream/fstream/
    sagt dazu:
    If the constructor is not successful in opening the file, the object is still created although no file is associated to the stream buffer and the stream's failbit is set (which can be checked with inherited member fail).

    Ups. Ich habe ganz vergessen, dass Exceptions bei Streams standardmäßig nicht verwendet werden. In diesem Fall ist das aber ein Schwäche von den Streams, welche RAII nicht 100%-ig sauber umsetzen.

    Skym0sh0 schrieb:

    wie sieht das denn analog dazu mit deinitialierungs methoden aus z.b. exit oder fstream::close(); ?

    Ähnlich. Wenn du das Objekt explizit schließt, könnte es passieren, dass du danach noch darauf zugreifst. Das wird verhindert, wenn das Schließen erst dann erfolgt, wenn das Objekt selbst zerstört wird.



  • Skym0sh0 schrieb:

    zum zweiten: also kann ich in meinen eigenen klassen ruhig initialisierungs methoden anbieten, aber auch neben einem konstruktor der diese itnern aufruft und damit das objekt schon bereit zur arbeit macht?
    (stimmt der technische aspekt denn, das open im konstruktor von fstream aufgerufen wird?)

    Grundsätzlich ist es sinnvoll Schnittstellen so anzulegen, das man sie möglichst nicht falsch benutzen kann. Ich bin z.B. kein Fan von separaten Initialisierungsroutinen (Zumal der Begriff eine Initialisierung andeutet, es kann aber nach dem Konstruktor nur noch ein Zuweisen/Überschreiben sein), falls solche wirklich nötig sind, greife ich zu Fabrikmethoden oder ähnlichen (Wo der Aufruf die Erzeugung beinhaltet, und ein gültiges Objekt (Meist über einen Smartpointer) zurück gibt.



  • ipsec schrieb:

    Ups. Ich habe ganz vergessen, dass Exceptions bei Streams standardmäßig nicht verwendet werden. In diesem Fall ist das aber ein Schwäche von den Streams, welche RAII nicht 100%-ig sauber umsetzen.

    kurz mal offtopic: ich hab mal irgendwo gelesen, dass exceptions bööse sind und von schlechtem design zeugen. stimmt das?
    oder ist es ok diese zu verwenden?



  • Skym0sh0 schrieb:

    ipsec schrieb:

    Ups. Ich habe ganz vergessen, dass Exceptions bei Streams standardmäßig nicht verwendet werden. In diesem Fall ist das aber ein Schwäche von den Streams, welche RAII nicht 100%-ig sauber umsetzen.

    kurz mal offtopic: ich hab mal irgendwo gelesen, dass exceptions bööse sind und von schlechtem design zeugen. stimmt das?
    oder ist es ok diese zu verwenden?

    Es ist absolut ok! Im Gegenteil, die Alternativen sind oft schlechtes Design. Die Quelle, wo du solche Aussagen gelesen hast, würde ich mit Vorsicht betrachten. RAII und Exceptions passen auch gut zusammen, zum einen bekommt man mit RAII Exceptionsicherheit fast geschenkt, zum anderen sind Exceptions die Lösung, um Fehler bei der Objektinitialisierung im Konstruktor anzuzeigen. Lediglich im Destruktor sollte man keine Exceptions werfen (übrigens noch ein Grund für close - in dem seltenen Fall, dass der Erfolg des Schließens wirklich von Belang ist, kann man es damit prüfen).



  • Ich denke "RAII" versteht jeder ein bisschen anders. Für mich ist es das Stichwort, womit ich folgendes in Verbindung bringe:

    Es ist oft extrem praktisch, die Verwaltung von Resourcen an Objekte und deren Lebenszeit zu knüpfen. Seien es Dinge wie Dateien oder auch dynamisch allozierte, andere Objekte bzw Speicherbereiche. Ich stelle mir diese Objekt/Besitz/Resourcen-Beziehungen gerne als Wald aus lauter Bäumen (bzw manchmal auch nur azyklische Graphen) vor, bei dem eine gerichtete Kante von x nach y bedeutet "x besitzt/verwaltet y". Den automatischen Speicher und den statischen Speicher zähle ich zu der Knotenmenge und sind bei mir immer die einzigen Wurzeln, da eine neue Resource entweder direkt in diesem Speicher lebt oder sofort mintestens einem anderen Besitzer zugeordnet wird. Hält man sich daran, wird sich das Thema "resource leaks" auch in Hinblick auf fliegende Ausnahmen erledigt haben. Das einzige, was da noch schief gehen kann, sind Dinge wie baumelnde Zeiger bzw Referenzen. Das kommt dann nur vor, wenn man Design-technisch Besitzverhältnisse falsch gewählt hat.

    Die Kurzfassung: RAII macht das Leben leichter, da die Verantwortung bzgl der Verwaltung von Resourcen an Objekte deligiert wird und man sich selbst nicht mehr darüber den Kopf so dolle zerbrechen muss, wann man wo etwas freigeben muss.

    Ob nun "Zwei-Phasen-Initialisierung" oder nicht, ist eine andere Geschichte. Ich würde hier immer noch von RAII sprechen. Ob eine Klasse noch einen "Nullzustand" bekommen soll oder nicht, muss man für sich im konkreten Fall entscheiden.

    Da ich Ausnahmebehandlung lieber mag als "if()goto", habe ich mir folgendes bei Funktionen angewöhnt, die von sich aus keine Ausnahme im Fehlerfall schmeissen:

    #if __cplusplus > 199800L
    #include <utility>
    #define MOVE(x) ::std::move(x)
    #else
    #define MOVE(x) (x)
    #endif
    
    namespace {
      void enforce(bool what, const char errmsg[])
      {
        if (!what) {
          string em = "Fehler in Modul XY: ";
          em += errmsg;
          throw my_module_specific_error(MOVE(em));
        }
      }
    }
    
    [....]
    
    /// @throws my_module_specific_error
    void dings::bums() {
      [....]
      ifstream ifs (dateiname);
      enforce(ifs,"konnte dies und das nicht oeffnen");
      [....]
    }
    


  • krümelkacker schrieb:

    Ob nunr "Zwei-Phasen-Initialisierung" oder nicht, ist eine andere Geschichte. Ich würde hier immer noch von RAII sprechen.

    Das ist auch meine Meinung - schön auf den Punkt gebracht.



  • krümelkacker schrieb:

    Ob nunr "Zwei-Phasen-Initialisierung" oder nicht, ist eine andere Geschichte. Ich würde hier immer noch von RAII sprechen. Ob eine Klasse noch einen "Nullzustand" bekommen soll oder nicht, muss man für sich im konkreten Fall entscheiden.

    "Zwei-Phasen-Initialisierung" ist eigentlich schon ein Widerspruch in sich. Mit einer Init Methode ist die Verwaltung der Ressource von der Lebensdauer des Objektes zumindest halb entkoppelt. Im Falle von fstream ist durch open() und close() praktisch alles von der Lebensdauer entkoppelt, das Objekt stellt nur noch die Freigabe der Ressource in jedem Fall sicher. Streng genommen entspricht das also nichtmehr der Bezeichnung RAII. Aber ja, es entspricht imo wohl noch dem wesentlichen Teil des Idioms. Vielleicht sollte man zwischen strong oder weak RAII unterscheiden oder so :p



  • Stroustrup sieht das ähnlich - bei RAII geht es um das Aufräumen angeforderter Ressourcen. Allerdings ist Stroustrup auch der Meinung, dass Objekte in der Regel gleich bei der Konstruktion initialisiert werden sollten (TC++PL E.3.5.3).

    Was Exceptions angeht, so ist RAII unverzichtbar für Exceptionsicherheit, aber RAII kann problemlos auch ohne Exceptions betrieben werden. Wann Exceptions zur Fehlerbehandlung Sinn machen, ist heftig umstritten und ein Stück weit Ansichtssache. Grob über den Daumen sehe ich es so, dass das Werfen von Exceptions dann Sinn macht, wenn ich in einer Situation bin, in der ich davon ausgehen kann, dass bei seinem Auftreten der umgebende Code nicht mehr vernünftig weiter machen kann. Etwa, wenn ich eine Datumsklasse schreibe und der umgebende Code versucht, den 53. Juni zusammenzustellen.



  • Nachtrag: Ich wollte hier krümelkacker zustimmen, aber da hat sich dot wohl dazwischengedrängt. 😞



  • seldon schrieb:

    Etwa, wenn ich eine Datumsklasse schreibe und der umgebende Code versucht, den 53. Juni zusammenzustellen.

    Ist hierfür ein assert() nicht besser geeignet?
    Meiner Meinung nach sollten Exceptions nur dort verwendet werden, wo die Zuständigkeit des Programmierers endet. Zum Beispiel bei IO.



  • Der 53. Juni kann ja nicht nur durch einen klasseninternen Fehler zustandekommen, sondern auch durch den Benutzer, z.B. wenn dieser versucht, die Instanz mit dem 53. Juni 2011 zu initialisieren. Eine Exception ist demnach angebracht.
    Edit: wobei man natürlich als Klassendesigner kein korrektes Verhalten garantieren muss, wenn man sie mit ungültigen Werten füttert. Damit wird die Eingabevalidation dem Nutzer der Klasse überlassen.



  • dot schrieb:

    krümelkacker schrieb:

    Ob nunr "Zwei-Phasen-Initialisierung" oder nicht, ist eine andere Geschichte. Ich würde hier immer noch von RAII sprechen. Ob eine Klasse noch einen "Nullzustand" bekommen soll oder nicht, muss man für sich im konkreten Fall entscheiden.

    "Zwei-Phasen-Initialisierung" ist eigentlich schon ein Widerspruch in sich. Mit einer Init Methode ist die Verwaltung der Ressource von der Lebensdauer des Objektes zumindest halb entkoppelt.

    Strenggenommen bräuchte man dann überhaupt keine Set-Methoden mehr?!



  • Warum das denn?
    Set-Funktionen ändern doch auch Werte, insb. auch solche Werte, die nur einen Default-Wert bekommen haben.


Anmelden zum Antworten