Smart Pointer statt 'new' und 'delete'?



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


  • Mod

    knivil schrieb:

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

    Er ruft Basisklassen- und Member-Destruktoren auf. Außerdem wird er im Standard als die Funktion behandelt, die die Lebenszeit eines Objektes beendet, sogar wenn er nichts tut.



  • knivil schrieb:

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

    Muss hier wirklich noch einmal undefiniertes Verhalten erklärt werden?
    Den Destruktor darf man genau dann aufrufen, wenn man zuvor das Objekt mit Placement new erfolgreich initialisiert hat.



  • @Arcoth: Danke!

    Recap: ptr_container, private Vererbung, mein grausamer Vorschlag mit clean(), delegating constructors.


  • Mod

    TyRoXx schrieb:

    Den Destruktor darf man genau dann aufrufen, wenn man zuvor das Objekt mit Placement new erfolgreich initialisiert hat.

    Ja, unter anderem.

    #include <iostream>
    
    int main()
    {
    	struct A
    	{
    		A() = default;
    		A( char const* str ) : str{str} {}
    		std::string str = "Hallo";
    	};
    
    	A a;
    	a.A::~A();
    	new (&a) A{"Hallihallo"};
    	std::cout << a.str;
    }
    

    Wohldefiniertes Verhalten.


Anmelden zum Antworten