Smart Pointer statt 'new' und 'delete'?



  • TyRoXx schrieb:

    hustbaer schrieb:

    Wenn man sowas 1-2 mal in einem Programm braucht, OK.

    Nein, das ist nicht OK. Das ist das ewige "aber ICH kann damit umgehen". Wieder die Frage: Warum korrekte und bewährte Hilfsmittel über Bord werfen für nichts?

    Hast du den Teil danach, den du nicht mehr zitiert hast, auch gelesen? Wenn ja, dann: hast du den Teil danach auch verstanden?

    Es ist immer ne Kosten/Nutzen Rechnung.
    Wenn die Kosten ausreichend klein sind es sauber zu machen, dann macht man es auf jeden Fall sauber. Wenn ich aber 1-2 Stellen in einem Programm hab', wo so eine Sauerei angesagt ist, weil die Kosten es sauber zu machen unangemessen hoch wären, dann mach ich mir keinen Kopf.
    Wenn du Herr Evangelist sein willst, und es trotz unvertretbarem Aufwand sauber implementieren willst, dann mach es ruhig.

    TyRoXx schrieb:

    hustbaer schrieb:

    @TyRoXx
    Nein, nix OOP, Implementierungsdetail. Die Hilfsklasse wird natürlich private geerbt.

    Ich wollte eigentlich schreiben, dass das ziemlich übler Missbrauch von Vererbung ist. Eine Basisklasse nur für einen Destruktor? WTF? Das ist die Art von "clever code", die man nicht haben will.

    Na bloss gut dass du's nicht gemacht hast! Das wäre nämlich eine ziemlich dumme Aussage gewesen. 😉

    Klassen die man nur oder hauptsächlich für ihren Destruktor verwendet sind in C++ nicht wirklich so selten. Speziell nicht wenn man die Welt der reinen Anwender verlässt und anfängt sich in der Welt der Library-Entwickler breitzumachen.

    Und es gibt auch keinen guten Grund auf sowas zu verzichten. Es ist einfach, es funktioniert und es ist wasserdicht/deppensicher.

    Wenn du andrer Meinung bist, dann zeig mir eine "bessere" Lösung, die ohne Einsatz von Hilfsmitteln wie ptr_container oder vector<unique_ptr<T>> auskommt.



  • hustbaer schrieb:

    Es ist immer ne Kosten/Nutzen Rechnung.
    Wenn die Kosten ausreichend klein sind es sauber zu machen, dann macht man es auf jeden Fall sauber. Wenn ich aber 1-2 Stellen in einem Programm hab', wo so eine Sauerei angesagt ist, weil die Kosten es sauber zu machen unangemessen hoch wären, dann mach ich mir keinen Kopf.
    Wenn du Herr Evangelist sein willst, und es trotz unvertretbarem Aufwand sauber implementieren willst, dann mach es ruhig.

    Die korrekten Lösungen, ptr_vector und vector<unique_ptr<>> , kosten nichts. Die unsauberen "Lösungen" kosten viel Zeit und Nerven und die wenigsten Programmierer können sie überhaupt fehlerfrei umsetzen.
    Mach doch mal bitte ein Beispiel für sauber vs unsauber. Ich weiß gar nicht was du meinst.



  • Ich meine, wie ich ja schon geschrieben habe, Fälle, wo die "korrekte" (lol BTW) Lösung eben nicht nichts kostet.
    Beispiel dafür müsste ich länger überlegen/rauskramen. Gibt aber so Fälle.

    Es ist mir aber echt Rätselhaft wie du auf Grund von meinen Beiträgen hier auf die Idee kommst, dass ich Gefrickel toleriere oder gar befürworte.



  • Ein vernünftiger Container allein reicht an dieser Stelle leider nicht aus. Bei

    nodes.push_back(new node(*p));
    

    könnte die node erstellt werden und push_back bad_alloc werfen, dann hat man ein Speicherleck. Was man hier eigentlich will, ist make_unique, was man sich bis C++14 halt selbst kurz zusammenkloppen muss. Oder halt in diesem Fall das Problem aus dem Weg räumen, indem man Vektor-Relokationen von vorneherein verhindert. boost::ptr_vector hat das selbe Problem, und durch die Vererbungsnummer (bzw. eine äquivalente Kompositionsnummer, wenn man den zusätzlichen Boilerplate in Kauf nehmen will) wird man es auch nicht sauber los.

    Als Beispiel für einen Fall, in dem der Vererbungstrick (oder eine äquivalente Kompositionsnummer) sinnvoll ist: ich musste vor inzwischen längerer Zeit mal einen Vektor-Ersatz schreiben, der seine Daten solange in einem lokalen aligned_storage hält, bis dieser voll ist und er auf den Heap ausweichen muss. Hintergrund war, dass sich in einem stark nebenläufigen Programm die Threads beim Locken des Heaps ständig gegenseitig im Weg standen und die Anzahl von Allokationen reduziert werden musste. Ich habe Exceptionsicherheit in den Konstruktoren hergestellt, indem ich die Speicherverwaltung in eine private Basisklasse ausgelagert habe. Einen std::vector zu halten, wäre unter den Umständen nicht sinnvoll gewesen.

    Was gerechtfertigte Frickelei angeht, habe ich einen ganz ähnlichen Fall mit gleicher Motivation: Diesmal geht es darum, Laufzeitpolymorphie ohne dynamische Speicherverwaltung zu bekommen. Die Typmenge ist vorher bekannt und statisch (sonst würde das auch nicht funktionieren), also läuft es im Wesentlichen darauf hinaus, ausreichend und passend ausgerichteten Speicher vorrätig zu haben. In C++11 lässt sich das wunderbar verpacken, in C++03 hat man das Perfect-Forwarding-Problem, wenn man ein Objekt in den Speicher instantiieren will. Wenn du eine saubere Lösung für das Problem hast, hab ich massig offene Ohren; ich hab das schlussendlich mit Makros gemacht, was bedeutet, dass einige Teile der Klassenvorlage public sein müssen, die es nicht sein sollten. Eine andere Lösung habe ich stumpf nicht gefunden, und "läuft, aber sieht nicht hübsch aus" ist immerhin besser als "läuft nicht mit dem Compiler, mit dem es laufen muss."



  • seldon schrieb:

    könnte die node erstellt werden und push_back bad_alloc werfen, dann hat man ein Speicherleck.

    Scheinproblem.
    Wie oft kommt sowas schon vor und welche sinnvolle Programmausführung ist dann noch möglich?



  • seldon schrieb:

    Ein vernünftiger Container allein reicht an dieser Stelle leider nicht aus. Bei

    nodes.push_back(new node(*p));
    

    könnte die node erstellt werden und push_back bad_alloc werfen, dann hat man ein Speicherleck. Was man hier eigentlich will, ist make_unique, was man sich bis C++14 halt selbst kurz zusammenkloppen muss. Oder halt in diesem Fall das Problem aus dem Weg räumen, indem man Vektor-Relokationen von vorneherein verhindert. boost::ptr_vector hat das selbe Problem, und durch die Vererbungsnummer (bzw. eine äquivalente Kompositionsnummer, wenn man den zusätzlichen Boilerplate in Kauf nehmen will) wird man es auch nicht sauber los.

    Dafür schreibt man sich eine push_back_or_delete Hilfsfunktion. Ist dann natürlich nicht mehr Deppensicher, weil jemand direkt push_back auf den vector versuchen könnte, aber hey.

    Oder man leitet beinhart vom vector ab, und überdeckt die push_back Funktion mit einer Variante die push_back_or_delete macht.



  • this->bin_tree::~bin_tree(); //undefiniertes Verhalten
    

    Ich habe nicht umsonst um Argumente aus dem Standard gebeten. Immer her damit, Begruendung! Alternativ kannst du auch eine nicht-virtuelle Funktion wie clear() etc. aufrufen wenn du dich am Destruktoraufruf stoerst. Die gibt es hoechst wahrscheinlich sowieso.

    Die korrekten Lösungen, ptr_vector und vector<unique_ptr<>>, kosten nichts

    Chef sagt: Kein Boost! Projekt mit VS 2008 oder schlimmer! Au backe. 🙂

    Hoffentlich! Die Lösung ist nämlich auch grausam.

    Auch so grausam ist das nicht, eher so mittelmaessig im Vergleich zu Code den ich schon sehen musste. Natuerlich darf der Konstruktor keine Exeption werfen, aber der Entwickler hat voll Kontrolle ueber die Hilfsklasse. Sollte also machbar sein.

    die wenigsten Programmierer können sie überhaupt fehlerfrei umsetzen

    Also ich traue es den meisten Entwicklern zu. Denn wenn nicht, dann muesste ich alles selbst machen.

    Klassen die man nur oder hauptsächlich für ihren Destruktor verwendet sind in C++ nicht wirklich so selten.

    Ja. 🙂

    Ich betrachte das sowieso leicht anders. Mir geht es in erster Linie darum, Moeglichkeiten auszuloten. Mit C++03 war es auch moeglich ohne shared_ptr, unique_ptr oder make_unique uebersichlichen, sauberen Code zu schreiben. Zusammenfassend haben wir ptr_container, private Vererbung und new (nothrow) , sofern keine weiteren Einwaende kommen. Ich fuer meinen Teil habe private Vererbung jetzt in mein Repertoire fuer RAII aufgenommen. Aber nicht, weil es mir unbekannt war, sondern ich normalerweise in andere Richtungen gedacht habe. Danke!

    Side node:

    der seine Daten solange in einem lokalen aligned_storage hält, bis dieser voll ist und er auf den Heap ausweichen muss

    tcmalloc/jemalloc je nach Anwendungsfall soll Wunder wirken.



  • qweasdyxc schrieb:

    seldon schrieb:

    könnte die node erstellt werden und push_back bad_alloc werfen, dann hat man ein Speicherleck.

    Scheinproblem.
    Wie oft kommt sowas schon vor und welche sinnvolle Programmausführung ist dann noch möglich?

    Kann ich als Grund nicht gelten lassen.

    Nicht nur weil wie oft es vorkommt und was danach noch möglich ist z.B. von der Grösse des vector s abhängt.
    Wenn der vector beim Reallocate gleich 500 MB auf einmal haben will, und wir es mit nem 32 Bit System zu tun haben...
    ...dann kann das schnell mal fehlschlagen, ohne dass deswegen die Ausführung des folgenden Error-Handling Codes und evtl. des restlichen Programms ein Problem haben müsste.

    Ist mMn. ne recht einfache Sache...
    Jemand der nicht bemerkt was da passieren könnte hat den falschen Job.
    Jemand der solche Dinge bemerkt, aber grundsätzlich drauf scheisst auch.
    Bleiben noch zwei evtl. akzeptable Unterkategorien: Programmierer die sich in jedem Fall immer wieder überlegen ob man es als Scheinproblem klassifizieren kann, und man sich daher die korrekte Implementierung sparen kann, und Programmierer die sich diese sinnlose Überlegung sparen und es gleich korrekt implementieren.

    Ich verschiebe die "zahlt sich das aus" Überlegung bei solchen Sachen auf jeden Fall soweit, bis ich merke, dass es wirklich viel Aufwand wäre. Was es in den seltensten Fällen ist. Spart mir viel sinnloses Kopfzerbrechen.



  • knivil schrieb:

    Auch so grausam ist das nicht, eher so mittelmaessig im Vergleich zu Code den ich schon sehen musste. Natuerlich darf der Konstruktor keine Exeption werfen, aber der Entwickler hat voll Kontrolle ueber die Hilfsklasse. Sollte also machbar sein.

    Klar, machbar ist viel. Eine Lösung die sich darauf verlässt dass eine bestimmte Funktion keine Exception wirft, ist aber zerbrechlicher, als eine Lösung der das egal ist. Also speziell in Hinblick auf spätere Modifikationen würde ich das nicht wollen.


  • Mod

    Ich habe nicht umsonst um Argumente aus dem Standard gebeten.

    Nun, es ist so:

    N3797 §12.4/15 schrieb:

    Once a destructor is invoked for an object, the object no longer exists; the behavior is undefined if the destructor is invoked for an object whose lifetime has ended (3.8).

    Das ist an sich völlig selbstverständlich. Nun kommt es darauf an, für was für ein Objekt der Kopierkonstruktor aufgerufen wird. Wenn es automatisch ist, oder automatisch zerstört werden wird, dann hat das Programm tatsächlich undefiniertes Verhalten. Eine clear() -Funktion wäre hier mMn. angebrachter.

    Klassen die man nur oder hauptsächlich für ihren Destruktor verwendet sind in C++ nicht wirklich so selten.

    Stimmt, die gibt es gleich in der Standardbibliothek: lock_guard zum Beispiel.



  • Nun kommt es darauf an, für was für ein Objekt der Kopierkonstruktor aufgerufen wird. Wenn es automatisch ist, oder automatisch zerstört wird, dann hat das Programm tatsächlich undefiniertes Verhalten

    Frage: Wenn der Konstruktor nicht ordnungsgemaess beendet wird, d.h. durch eine Exception abgebrochen wurde, wird dann der Destruktor aufgerufen. Ich denke nicht, da das Objekt ja nicht konstruiert wurde (Member werden natuerlich ordnungsgemaess zertoert). Man koennte noch ueber Faelle mit Vererbung nachdenken, aber hier wuerde ich dann doch eine clean-Funktion vorziehen.

    If a constructor throws an exception, the object's destructor is not run.

    http://www.parashift.com/c++-faq/selfcleaning-members.html


  • Mod

    Ich denke nicht, da das Objekt ja nicht konstruiert wurde (Member werden natuerlich ordnungsgemaess zertoert).

    Ja, natürlich.

    An object of any storage duration whose initialization [..] is terminated by an exception will have destructors executed for all of its fully constructed subobjects [..].
    Similarly, if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked.
    If the object was allocated in a new-expression, the matching deallocation function [..] is called to free the storage occupied by the object.


  • Mod

    Da ist übrigens nach wie vor undefiniertes Verhalten, da du einen Destruktor für ein nicht existierendes Objekt aufrufst.



  • Arcoth schrieb:

    Da ist übrigens nach wie vor undefiniertes Verhalten, da du einen Destruktor für ein nicht existierendes Objekt aufrufst.

    Muss ich denn jedesmal nach der Argumentation fragen, also nach dem Standard, da du ihn ja grade sowieso zur Hand hast ... jaja, ich rufe clean() auf. 🙂

    if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked.

    D.h. in dem Fall von bin_tree und C++11, dass man das Objekt mittels einfachem Konstruktor konstruieren kann und dann im Kopierkonstruktor sicher seine Exception werfen kann, d.h.

    bin_tree::bin_tree(bin_tree const &other): bin_tree() { // give me a valid object for deconstruction
        nodes.resize(other.nodes.size(),0) 
        for(unsigned int i = 0; i < nodes.size(); ++i) { 
            nodes[i] = new node(other.nodes[i]);        } 
        } 
    }
    

    es wuerde bei Fehlschlagen von resize oder new mittels Exception trotzdem der Destruktor aufgerufen werden.



  • @knivil: Was meinst du, passiert, wenn ich dann

    std::unique_ptr<bin_tree> p(new bin_tree(some_other_tree));
    

    schreibe und bin_trees Konstruktor schmeißt? Wer räumt den von operator new angeforderten Speicher wieder weg?


  • Mod

    Muss ich denn jedesmal nach der Argumentation fragen, also nach dem Standard

    .. ich finde kein bestätigendes Zitat, obwohl ich todsicher überzeugt bin, dass man im Konstruktorrumpf den Destruktor nicht aufrufen darf. Wenn das kein UB ist, dann fresse ich einen Besen. Ich finde aber einfach nichts... 😞

    Eins steht fest: Anscheinend "existiert" das Objekt, denn im Konstruktor kann man natürlich auf seine Member zugreifen. In 12.6 und 12.7 finde ich auch nichts.


  • Mod

    Wer räumt den von operator new angeforderten Speicher wieder weg?

    Na die Implementierung 😉

    §15.2/2 schrieb:

    If the object was allocated in a new-expression, the matching deallocation function (3.7.4.2, 5.3.4, 12.5), if any, is called to free the storage occupied by the object.



  • Ach, verdammt. Ja, du hast natürlich recht; ich hab mich da böse verdacht.

    Aber die Member des Objektes, die bereits konstruiert wurden, werden auf die Weise doppelt zerstört, oder? Koscher sieht das auf jeden Fall nicht aus.


  • Mod

    seldon schrieb:

    Aber die Member des Objektes, die bereits konstruiert wurden, werden auf die Weise doppelt zerstört, oder?

    Ja, richtig! Da ist ja der entscheidende Punkt, den ich gesucht habe. Einmal durch den expliziten Destruktoraufruf, und einmal durch das Verlassen des Konstruktors durch die Exception. 💡



  • Der Destruktor ist auch nur eine Funktion, wo ist das Problem?


Anmelden zum Antworten