C++ ... unbeholfen bei der Objektorientierung. Fehler beim Client.



  • unskilled schrieb:

    Hacker schrieb:

    Was werfen denn String und Vector, wenn du

    std::vector<int> a(10);
    
    a.at(20);
    

    machst?

    Er meinte mit Sicherheit das
    type& operator[](const std::size_t& index) throw(std::out_of_range)
    es gibt schon 12387 diskussionen über exception specifications, aber bisher kam man glaube ich jedes mal auf den schluss, lieber
    type& operator[](const std::size_t& index) /*throw(std::out_of_range)*/
    zu schreiben.

    bb

    Ah, gut! Danke werds mir merken :xmas1:


  • Mod

    Zumal es nun sogar offiziell deprecated ist, das so zu schreiben.



  • Quatsch, mal wieder, leider.

    314159265358979 schrieb:

    hustbaer schrieb:

    void Clear()
        {
            delete [] arr;
            arr = 0;
            len = 0;
        }
    
        Array& operator=(const Array& other)
        {
            Clear();
            arr = new type[other.len]; // erst arr setzen, damit verbleibt das Objekt immer in einem gültigen Zustand, auch wenn es hier schnaltz
            len = other.len;           // würde man erst len setzen, könnte man arr = 0, len = 42 haben - was dann zu Problemen im operator[] führen könnte
            for (std::size_t i = 0; i < len; i++)
                arr[i] = other.arr[i];
        }
    

    Ist auch nicht Exception-safe. Besser:

    void Clear()
    {
        delete[] arr;
        arr = 0;
        len = 0;
    }
    
    Array& operator = (Array const& other)
    {
        type* ptr = new type[other.len]; // Gleich mal als erstes den Speicher allozieren, um die starke Garantie geben zu können
        Clear();
        arr = ptr;
        len = other.len;
        std::copy(other.begin(), other.end(), arr);
        return *this;
    }
    

    Erklär mir mal bitte wo mein Code nicht exception-safe ist.

    Er erfüllt die "basic guarantee": das Objekt verbleibt auf jeden Fall in einem gültigen Zustand, und es gibt keine Leaks. Dass der gültige Zustand im Fall eine Exception nicht unbedingt dem entspricht, was sich der Aufrufer erwartet, ist für die "basic guarantee" vollkommen irrelevant.

    Dein Code dagegen versucht die "strong guarantee" zu geben, scheitert dabei aber kläglich. Nämlich genau dann, wenn std::copy() eine Exception wirft. Was ja ohne weiteres sein kann, da es sich um ein Template handelt, und type::operator = ja Exceptions werfen kann. Dann ist nämlich ein Teil des Arrays kopiert, ein Teil aber nicht (=Elemente wurden Default-Initialisiert, aber danach nicht zugewiesen).

    Wenn man die "strong guarantee" geben will, dann nimmt man am besten Copy & Swap dafür. Was ich ja auch schon angesprochen hatte.

    ps: streng genommen hab ich sogar einen Fehler in meinem Code, den du auch nicht ausgebessert hast: es dürfen ja auch Destruktoren Exceptions werfen. Das ist zwar ganz viel pfui, aber grundsätzlich erlaubt. D.h. Clear() könnte (sollte?) man vielleicht folgendermassen anpassen:

    void Clear()
        {
            type* to_delete = arr;
            arr = 0;
            len = 0;
            delete [] to_delete;
        }
    


  • @unskilled:
    Naja, copy & swap hatte ich ja schon erwähnt.
    Wobei dein Code auch einen Bug hat (neben den Tippfehlern und dem nicht optimalen re-throw im Ctor):

    clear() muss schon arr und len auf Null setzen, sonst zerstört es ja die Invarianten.

    Und zwecks besserer Optimierbarkeit sollte man den operator = so schreiben:

    Array& operator = (Array other) 
    	{ 
    		swap(other);
    		return *this;
    	}
    

    ps: um das re-throw im Ctor loszuwerden muss man wohl ober übel eine Hilfsklasse verwenden, die das im Destruktor erledigt. Diese Hilfsklasse kann man dann entweder als Basisklasse, Member oder lokalen Guard einbinden (ich würde es vermutlich als Member machen).

    Der Sinn ist, dass man kein try-catch braucht. Das hat nämlich Vorteile, wenn die Exception nirgends gefangen wird. Dann bleibt der Debugger nämlich dort stehen, wo die "primäre" Exception geworfen wird, und nicht beim "throw;" in deinem catch-Handler.
    Weiters führt es (je nach Compiler) dazu, dass beim "freien" Ausführen des Programms kein (teilweises) Stack-Unwinding mehr gemacht wird, bevor das Programm abgebrochen wird, was auch Vorteile haben kann.



  • hustbaer schrieb:

    Quatsch, mal wieder, leider.

    Es heißt "Quark, mal wieder, leider.". Installier dir das Script oder spar's dir.

    hustbaer schrieb:

    Er erfüllt die "basic guarantee": das Objekt verbleibt auf jeden Fall in einem gültigen Zustand, und es gibt keine Leaks. Dass der gültige Zustand im Fall eine Exception nicht unbedingt dem entspricht, was sich der Aufrufer erwartet, ist für die "basic guarantee" vollkommen irrelevant.

    Warum die basic guarantee geben, wenn man strong geben kann? Depp.

    hustbaer schrieb:

    Dein Code dagegen versucht die "strong guarantee" zu geben, scheitert dabei aber kläglich. Nämlich genau dann, wenn std::copy() eine Exception wirft. Was ja ohne weiteres sein kann, da es sich um ein Template handelt, und type::operator = ja Exceptions werfen kann. Dann ist nämlich ein Teil des Arrays kopiert, ein Teil aber nicht (=Elemente wurden Default-Initialisiert, aber danach nicht zugewiesen).

    Daran habe ich nicht gedacht, da hast du Recht. Trotzdem ist mein Code schon ne Stufe besser als deiner.

    hustbaer schrieb:

    ps: streng genommen hab ich sogar einen Fehler in meinem Code, den du auch nicht ausgebessert hast: es dürfen ja auch Destruktoren Exceptions werfen. Das ist zwar ganz viel pfui, aber grundsätzlich erlaubt. D.h. Clear() könnte (sollte?) man vielleicht folgendermassen anpassen:

    Quatsch. Wenn jemand Exceptions in Dtoren wirft, ist er selbst schuld. Auch die Standardcontainer geben in so einem Fall keine Garantien mehr.



  • hustbaer schrieb:

    void Clear()
        {
            delete [] arr;
            arr = 0;
            len = 0;
        }
    
        Array& operator=(const Array& other)
        {
            Clear();
            arr = new type[other.len]; // erst arr setzen, damit verbleibt das Objekt immer in einem gültigen Zustand, auch wenn es hier schnaltz
            len = other.len;           // würde man erst len setzen, könnte man arr = 0, len = 42 haben - was dann zu Problemen im operator[] führen könnte
            for (std::size_t i = 0; i < len; i++)
                arr[i] = other.arr[i];
        }
    

    Plus Check auf Selbstzuweisung, weil sonst die Selbstzuweisung arr leert.



  • Quark, mal wieder, leider.

    314159265358979 schrieb:

    Warum die basic guarantee geben, wenn man strong geben kann? Depp.

    hustbaer schrieb:

    Dein Code dagegen versucht die "strong guarantee" zu geben, scheitert dabei aber kläglich.[...]

    Daran habe ich nicht gedacht, da hast du Recht.

    Fällt dir nichts aus? Oder warst du zu sehr damit beschäftigt, Schimpfwörter zu wählen? Man sieht mal wieder, dass deine guten Vorsätze nicht von weit her sind.



  • *gähn



  • Man kann kaum glauben, dass du mal rumgeheult hast, dass du nicht verstehst, warum du unbeliebt bist, und dass du ja eigentlich ein ganz Freundlicher bist und in Zukunft nett sein willst.



  • @Michael E.:
    Ja, check auf Selbstzuweisung wäre gut. Hab ich nicht dran gedacht. Danke für den Hinweis.



  • Wie man in den Wald hineinruft, so schallt es wieder raus.



  • 314159265358979 schrieb:

    Wie man in den Wald hineinruft, so schallt es wieder raus.

    Ich hab nichts gegen Leute die Selbstgespräche führen.



  • talktoyou schrieb:

    314159265358979 schrieb:

    Wie man in den Wald hineinruft, so schallt es wieder raus.

    Ich hab nichts gegen Leute die Selbstgespräche führen.

    Ich hab auch nichts gegen Leute die Leute die Selbstgespräche führen auf eine subtile Weise kritisieren, nein ganz und gar nicht :xmas1: :xmas2:



  • Ist die Kiddierunde jetzt bald komplett? 😕



  • jklkl schrieb:

    Ist die Kiddierunde jetzt bald komplett? 😕

    Jetzt schon. :xmas1:

    Edit: Nein, du hast ja recht :seufz: 😞



  • clear() muss schon arr und len auf Null setzen, sonst zerstört es ja die Invarianten.

    was sind invarianten? ich konnt auch bei google via invariants nichts auf die schnelle finden. des weiteren sollte es ja reichen, length auf 0 zu setzen
    auf anhieb ist mir jetzt auch kein szenario eingefallen, wo es irgendetwas kaputt macht.

    tippfehler

    assert(index >= len); ist natürlich falsch, sry 😃 war aber eher nen flüchtigkeitsfehler als nur ein tippfehler: assert(index < len);

    ps: um das re-throw im Ctor loszuwerden muss man wohl ober über eine Hilfsklasse verwenden, die das im Destruktor erledigt. Diese Hilfsklasse kann man dann entweder als Basisklasse, Member oder lokalen Guard einbinden.

    Der Sinn ist, dass man kein try-catch braucht. Das hat nämlich Vorteile, wenn die Exception nirgends gefangen wird. Dann bleibt der Debugger nämlich dort stehen, wo die "primäre" Exception geworfen wird, und nicht beim "throw;" in deinem catch-Handler.
    Weiters führt es (je nach Compiler) dazu, dass beim "freien" Ausführen des Programms kein (teilweises) Stack-Unwinding mehr gemacht wird, bevor das Programm abgebrochen wird, was auch Vorteile haben kann.

    hab ich auch noch nie was von gehört; klingt aber höchst interessant. hast du nen link dazu? wäre toll 😉

    danke schon mal,
    bb



  • unskilled schrieb:

    clear() muss schon arr und len auf Null setzen, sonst zerstört es ja die Invarianten.

    was sind invarianten? ich konnt auch bei google via invariants nichts auf die schnelle finden.

    Invarianten sind Bedingungen, die immer erfüllt sein müssen:
    http://de.wikipedia.org/wiki/Invariante_(Informatik)

    Wenn diese Invarianten den Zustand eines Objekts "einschränken", nennt man sie "class invariant":
    http://en.wikipedia.org/wiki/Class_invariant

    In dem Beispiel hier, kann man davon ausgehen, dass eine solche Invariante len == 0 || arr != 0 ist.
    Weitere die sich in C++ allerdings nicht so ohne weiteres prüfen lassen wären: arr == 0 || is_valid_array_pointer(arr) oder len == 0 || allocated_size(arr) >= len .

    Das steht zwar in diesem Beispiel nirgends, aber es ist vernünftig es anzunehmen. Bzw. man kann es auch mehr-oder-weniger aus dem Code der Klasse rauslesen.

    Kurz: die Klasseninvarianten definieren was "gültiger Zustand" für Objekte der Klasse bedeutet.

    des weiteren sollte es ja reichen, length auf 0 zu setzen
    auf anhieb ist mir jetzt auch kein szenario eingefallen, wo es irgendetwas kaputt macht.

    Nö, reicht nicht.
    Würde reichen, wenn man Clear() so implementieren würde

    void Clear()
    {
        if (len)
        {
            delete [] arr;
            arr = 0; // jetzt optional
            len = 0;
        }
    }
    

    Sonst würde bei mehrmaligem Aufruf von Clear() ja das bereits gelöschte Array nochmal gelöscht.

    Und den Assignment-Operator müsste man so anpassen, dass er erstmal guckt ob "other.len" auch nicht 0 ist, und wenn doch auch das arr = new type[other.len] überspringen. Sonst würde dort ein "zero size array" erzeugt, das nie mehr freigegeben wird. Weil wir ja Clear() dahingehend modifizieren mussten, dass Clear() das nicht mehr machen würde, soband len == 0 .

    MMn. ist es da viel einfacher, die Invariante arr == 0 || is_valid_array_pointer(arr) zu wählen. Also in Worten: wenn "arr" nicht Null ist, muss es auch auf was gültiges zeigen.

    tippfehler

    assert(index >= len); ist natürlich falsch, sry 😃 war aber eher nen flüchtigkeitsfehler als nur ein tippfehler: assert(index < len);

    Ich meinte das ref.l im Copy-Ctor. Die Klasse hat kein Member namens "l". Das war natürlich auch ein Flüchtigkeitsfehler und kein Tippfehler.

    ps: um das re-throw im Ctor loszuwerden (...)

    hab ich auch noch nie was von gehört; klingt aber höchst interessant. hast du nen link dazu? wäre toll 😉

    Link finde ich auf die Schnelle keinen.
    Worum es geht hab' ich aber eh schon beschrieben.
    Die Sache mit dem Debugger sollte klar sein.

    Und die andere Sache, also dass es günstig sein kann wenn kein Stack-Unwinding passiert bevor terminate() aufgerufen wird...

    Zwei Dinge: erstmal kann man ja nen eigenen Handler für terminate() definieren. Da drinnen kann man z.B. nen Crashdump schreiben, und nach Hause mailen/FTPen/POSTen. Der Vorteil von "kein Unwinding" ist dann der selbe wie beim "live" Debuggen: man sieht im Debugger, wenn man den Crashdump aufmacht, den Zustand des Programms zu dem Zeitpunkt wo der "ursprüngliche" Fehler passiert ist, und nicht einem, wo schon Stackunwinding zur "rethrow" Stelle gemacht wurde.

    Der zweite Grund ist etwas umstritten...

    Wenn ein Programm eine Exception wirft, die niemand fängt, dann bedeutet das normalerweise einen Programmierfehler. Das Programm hat also etwas gemacht, womit der Programmierer nicht gerechnet hat. Und es soll abgebrochen werden.

    Jetzt ist die Frage: soll man in dem Zustand noch weiteren Code ausführen, der sich darauf verlässt dass sämtliche Invarianten noch gültig sind? (Also dass alle Objekte bzw. das gesamte Programm sich noch in einem gültigen Zustand befinden.)

    Und das halte ich für eine halbwegs fragwürdige Sache, zumal ja bereits etwas passiert ist, mit dem keiner gerechnet hat. Das Programm befindet sich also garantiert in einem irgendwie "ungültigen" Zustand, sonst hätte es nie einen Punkt erreicht, an dem eine Exception geworfen wird, die keiner fängt.
    Sich dann darauf zu verlassen dass das Stack-Unwinding bis zu einem bestimmten Punkt noch klappt, und dabei kein Unsinn produziert wird, ist ein halbwegs gewagtes Pokerspiel.


Anmelden zum Antworten