Wozu braucht man exceptionsichere assignment Operatoren?



  • antenna schrieb:

    Ich weiß worum es geht und darum auch meine Ausgangsfrage: "Hat jemand ein realistisches Beispiel, bei dem er nach einer Exception noch mit dem Objekt weiter arbeitet?" Das einzige Beispiel was bis jetzt kam war "die Objekte wieder sauber abräumen", was aber nur in den Copy-Ctor mit RAII verschoben wird. Und meine Ausgangsfrage ist immer noch offen.

    exception im operator= oder generell?
    der operator= ist natürlich etwas sehr kritisches, wenn der fehlschlägt ist die chance dass du mit dem objekt noch viel machen kannst eher gering. denn der grund warum ein op= fehlschlägt ist idr ein gravierender.

    weiter arbeiten muss man aber aufjedenfall im dtor. das ist immer der fall. meistens macht man dann ein rollback der letzten aktionen (copy&swap ftw).

    aber ich verstehe generell nicht warum du dich so darauf versteifst. imho ist das einfach eine komplett falsche fragestellung die am kern des themas komplett vorbei geht. beispiele für einen op= der fehlschlägt zu liefern ist nicht das problem, das problem ist dein verständnis eines "weiter arbeitens".

    stell dir einfach ein objekt vor, dass du serialisierst und du willst es kopieren. irgendwas geht dabei schief und du musst es nachher natürlich wieder serialisieren. wenn da das objekt in keinem gültigen zustand ist, ist deine anwendung bis zur nächsten neuinstallation defekt.



  • Shade Of Mine schrieb:

    stell dir einfach ein objekt vor, dass du serialisierst und du willst es kopieren. irgendwas geht dabei schief und du musst es nachher natürlich wieder serialisieren. wenn da das objekt in keinem gültigen zustand ist, ist deine anwendung bis zur nächsten neuinstallation defekt.

    In Code übersetzt würde das z.B. so aussehen.

    Objekt o = load();
    	Objekt newO = getNew();
    	try 
    	{
    		o = newO;
    	}
    	catch(...) 
    	{
    	}
    	save(o); //Dank Copy&Swap können wir hier speichen, da wir auf jeden Fall ein gültiges Objekt haben, egal ob in der Zuweisung was scheif ging.
    

    Aber wer programmiert denn so?
    Eigentlich programmiert man doch so, dass garnicht erst gespeichert wird, wenn was schief ging.

    Objekt o = load();
    	Objekt newO = getNew();
    	o = newO; // Wenn hier eine Exception fliegt, wird save nie aufgerufen
    	save(o);
    


  • Was ist aber, wenn du z.B eine sinnvolle Loginausgabe machen willst des Objektes, welches eine Exception geworfen hat?
    Oder um bei dem Beispiel des Speicherns zu bleiben: Du bearbeitest z.B gerade ein Dokument und irgendwas geht schief und aus irgendeinem Grund muss das Programm beendet werden. Jetzt kannst du schön den Fehler abfangen und kurz speichern oder mal eben den User fragen, was er machen möchte.



  • drakon schrieb:

    Was ist aber, wenn du z.B eine sinnvolle Loginausgabe machen willst des Objektes, welches eine Exception geworfen hat?

    Ja, logging des alten Zustandes kann nur funktionieren, wenn man nen exceptionsicheren assignment Operator hat. Aber sein mal ehrlich, wie oft hast du nur um eine Zuweisung ein try-catch gemacht, um dann zu loggen?

    Oder um bei dem Beispiel des Speicherns zu bleiben: Du bearbeitest z.B gerade ein Dokument und irgendwas geht schief und aus irgendeinem Grund muss das Programm beendet werden. Jetzt kannst du schön den Fehler abfangen und kurz speichern oder mal eben den User fragen, was er machen möchte.

    Das "irgendwas geht schief" ist schon wieder viel zu ungenau. Es geht ja um Exceptions im Assignment Operator. Mach doch mal ein realistisches Codebeispiel.



  • Shade hat ja bereits etwas zu dem Operator gesagt:

    Shade Of Mine schrieb:

    der operator= ist natürlich etwas sehr kritisches, wenn der fehlschlägt ist die chance dass du mit dem objekt noch viel machen kannst eher gering. denn der grund warum ein op= fehlschlägt ist idr ein gravierender.

    Ich bin daher von der grundsätzlichen Variante ausgegangen..

    Realistisch ist imo so etwas:

    Object o;
    try
    {
      //...
      o.try_something_that_might_throw ();
      //...
    }
    catch (...)
    {
     // ok ging wohl nicht..
     cout << o.debug_information (); // was für den Entwickler
     save_states_to_file ( o.get_states () ); // und was für den User
    }
    

    So in etwa mache ich das auch, wenn ich lokaler etwas behandeln kann.

    Das war ja deine ursprüngliche Frage, oder nicht?



  • antenna schrieb:

    Ja, logging des alten Zustandes kann nur funktionieren, wenn man nen exceptionsicheren assignment Operator hat. Aber sein mal ehrlich, wie oft hast du nur um eine Zuweisung ein try-catch gemacht, um dann zu loggen?

    Nicht um die Zuweisung. Aber um eine andere Operation. Die muss auch exceptionsicher sein. Und das geht nur wenn die verwendeten Operationen exceptionsicher sind, unter anderem die verwendeten op=



  • drakon schrieb:

    Realistisch ist imo so etwas:

    Object o;
    try
    {
      //...
      o.try_something_that_might_throw ();
      //...
    }
    catch (...)
    {
     // ok ging wohl nicht..
     cout << o.debug_information (); // was für den Entwickler
     save_states_to_file ( o.get_states () ); // und was für den User
    }
    

    So in etwa mache ich das auch, wenn ich lokaler etwas behandeln kann.

    Das war ja deine ursprüngliche Frage, oder nicht?

    Ne, ich seh gerade, dass die Frage auch etwas schlecht gestellt ist, weil sie nur im Zusammenhang mit dem Threadtitel Sinn macht. Es geht um Beispiele wozu man exceptionsichere assignment Operatoren braucht, um dann noch das korrekte alte Objekt zu haben, obwohl eine Exception beim Zuweisen geflogen ist.

    Mir ist ein Beispiel eingefallen das möglich wäre, aber dass ich auch nicht für sehr realistisch halte.
    Man möchte z.B. eine state machine die immer den alten Zustand behält, wenn man den neuen Zustand nicht schalten kann.

    try
    {
        m_currentState = m_stateA;
    }
    catch(...)
    {
       log("State change failed, old state remains. ")
    }
    

    Aber warum sollte man immer ein ganzes Objekt kopieren um einen State zu schalten. Einfach nur nen Pointer umsetzen wäre viel performanter und dabei kann auch keine Exception fliegen. Und bei welchem Anwendungsfall will man schon immer den alten State behalten?

    Die einzigen Sachen die mir einfallen, die exceptionsichere assignment Operatoren haben sollten, sind Libraries die praktisch für alles verwendet werden können. Da könnte ja mal jemand ein Anwendungsfall einfallen, wo er das braucht. Was aber nicht meine Frage nach so einem Anwendungsfall beantwortet. 😃

    pumuckl schrieb:

    antenna schrieb:

    Ja, logging des alten Zustandes kann nur funktionieren, wenn man nen exceptionsicheren assignment Operator hat. Aber sein mal ehrlich, wie oft hast du nur um eine Zuweisung ein try-catch gemacht, um dann zu loggen?

    Nicht um die Zuweisung. Aber um eine andere Operation. Die muss auch exceptionsicher sein. Und das geht nur wenn die verwendeten Operationen exceptionsicher sind, unter anderem die verwendeten op=

    Du hast also eine Operation bei der du ein Objekt übergibst und weist dem was zu und wenn eine Exception vor oder während dem Zuweisen fliegt, willst du den alten Zustand loggen, wenn die Exception später fliegt, willst du den neu zugewiesenen Zustand loggen? Oder meinst du was anderes? Mach einfach ein Codebeispiel.



  • Naja, so was gibt es schon öfters:

    while(obj.isValid()) {
        obj.machWas();
        obj = obj.next();
    }
    


  • antenna schrieb:

    Es geht um Beispiele wozu man exceptionsichere assignment Operatoren braucht, um dann noch das korrekte alte Objekt zu haben, obwohl eine Exception beim Zuweisen geflogen ist.

    Die Notwendigkeit, Zuweisungsoperatoren exceptionsicher zu gestalten, findest du bereits in grundlegendsten Algorithmen. Nimm std::copy() . Diese Funktion bietet die Basis-Exceptionsicherheit (komplettes Rollback ist nicht möglich, aber es muss nach wie vor ein gültiger Zustand herrschen, d.h. keine halben Objekte). Um dies zu bewerkstelligen, muss das Element, dessen Zuweisung eine Exception wirft, unbedingt im alten Zustand bleiben.

    Oder sag mir, wie du std::copy() implementierst, wenn der Zuweisungsoperator nicht exceptionsicher ist. Die Funktion copy() ist übrigens bei Weitem kein Einzelfall, in der STL findest du viele ähnliche Beispiele.



  • Nexus schrieb:

    antenna schrieb:

    Es geht um Beispiele wozu man exceptionsichere assignment Operatoren braucht, um dann noch das korrekte alte Objekt zu haben, obwohl eine Exception beim Zuweisen geflogen ist.

    Die Notwendigkeit, Zuweisungsoperatoren exceptionsicher zu gestalten, findest du bereits in grundlegendsten Algorithmen. Nimm std::copy() . Diese Funktion bietet die Basis-Exceptionsicherheit (komplettes Rollback ist nicht möglich, aber es muss nach wie vor ein gültiger Zustand herrschen, d.h. keine halben Objekte). Um dies zu bewerkstelligen, muss das Element, dessen Zuweisung eine Exception wirft, unbedingt im alten Zustand bleiben.

    Oder sag mir, wie du std::copy() implementierst, wenn der Zuweisungsoperator nicht exceptionsicher ist. Die Funktion copy() ist übrigens bei Weitem kein Einzelfall, in der STL findest du viele ähnliche Beispiele.

    Gerade das, was es nicht kann, nämlich den ganzen Rollback, wäre das interessante. Und zu solchen Libraries ahb ich ein paar Zeilen weiter unten schon was gesagt.



  • antenna schrieb:

    Gerade das, was es nicht kann, nämlich den ganzen Rollback, wäre das interessante.

    Das ist nun mal nicht sinnvoll möglich, zumindest nicht bei der Generizität.

    Ich habe dir ein Beispiel für "wozu man exceptionsichere assignment Operatoren braucht, um dann noch das korrekte alte Objekt zu haben, obwohl eine Exception beim Zuweisen geflogen ist" gebracht. Ich bezog mich dabei auf das einzelne Element und nicht auf die Sequenz als Ganzes. Wenn dir das nicht ausreicht, musst du schon konkreter werden.



  • antenna schrieb:

    Gerade das, was es nicht kann, nämlich den ganzen Rollback, wäre das interessante.

    Eine Operation, in der mehrere Suboperationen Exceptions werfen können, ist per sé nicht garantiert rollbackfähig. Wenn eine Operation ohne nothrow-Garantie abgeschlossen ist, kann sie nicht mit nothrow-Garantie rückgängig gemacht werden, mit Ausnahme von werfenden Konstruktor-artigen Operationen (inklusive Ressourcenacquisition), da Destruktoren und Ressourcenfreigabe grundsätzlich nicht werfen sollen.

    Ein std::copy rollbackfähig zu gestalten bräuchte daher eine zwischengeschaltete temporäre Operation, die das Ziel nicht verändert, also eine temporäre Sequenz vom gleichen Typ wie die Zielsequenz, so dass erst kopiert und dann die komplette Sequenz geswappt werden kann.


  • Administrator

    pumuckl schrieb:

    Ein std::copy rollbackfähig zu gestalten bräuchte daher eine zwischengeschaltete temporäre Operation, die das Ziel nicht verändert, also eine temporäre Sequenz vom gleichen Typ wie die Zielsequenz, so dass erst kopiert und dann die komplette Sequenz geswappt werden kann.

    Was dann der Copy&Swap Technik entspricht 😃
    Also grundsätzlich sehr schön zu sehen:

    std::vector<int> source; // mit irgendwelchen Daten gefüllt.
    std::vector<int> destination;
    
    destination = source;
    // Wenn bei diesem operator= Copy&Swap eingesetzt wurde,
    // dann hat man die gesamte Rollback-Möglichkeit, welche
    // antenna so dringend sehen wollte :)
    

    Grüssli



  • Dravere schrieb:

    Was dann der Copy&Swap Technik entspricht 😃
    Also grundsätzlich sehr schön zu sehen:

    std::vector<int> source; // mit irgendwelchen Daten gefüllt.
    std::vector<int> destination;
    
    destination = source;
    // Wenn bei diesem operator= Copy&Swap eingesetzt wurde,
    // dann hat man die gesamte Rollback-Möglichkeit, welche
    // antenna so dringend sehen wollte :)
    

    Was aber nicht trivial über die Copy&Swap-implementierten op= der einzelnen Elemente zu machen ist sondern ein copy&swap auf anderer Ebene.

    In den Beiträgen weiter oben gings um die algorithmen, z.B. std::copy, die einfach zu generisch sind um da sinnvoll ein copy&swap auf sequenzebene hinzubekommen, zumal der Algorithmus nur den Iteratortyp benutzt und auf die dahinter liegende Sequenz keinen Zugriff hat. Bei einem std::copy auf einem simplen array als target wäre ein copy&swap auf Sequenzebene komplett unmöglich.

    bei std::vector gilt außerdem das "don't pay for what you don't need"-Prinzip, daher wird auch hier kein copy&swap drin sein, auch wenns möglich wäre. Wenn destination nämlich bereits genug kapazität hat, verbietet dieses Prinzip, temporär neuen Speicher für die Kopie der sequenz vor dem swap anzulegen. Manuell ist das zumindest für std::vector allerdings möglich, da Copy-Ctor und swap-Methode zur Verfügung stehn.

    destination.swap(std::vector<int>(source));
    

    Das reduziert ggf. die Kapazität von destination und hat damit andere Semantik als der eingebaute op=.



  • antenna schrieb:

    Gerade das, was es nicht kann, nämlich den ganzen Rollback, wäre das interessante. Und zu solchen Libraries ahb ich ein paar Zeilen weiter unten schon was gesagt.

    trivial:

    template<typename InIter, typename OutIter>
    void rollback_copy(InIter begin, InIter end, OutIter out) {
      std::vector<typename iterator_traits<InIter>::value_type> temp(begin, end);
      InIter i=temp.begin();
      InIter e=temp.end();
      while(i!=e) {
        swap(*i, *out);
        ++i; ++out;
      }
    }
    

    verlangt natürlich eine menge sachen und ist deshalb nicht wirklich so toll. denn out muss zB auf genug fertig konstruierte objekte zeigen. dass muss es bei copy nicht (man kann ja nen inserter verwenden oder ähnliches). aber genau so geht ein rollback. natürlich verwendet diese funktion das objekt in dessen op= die exception geflogen ist nur zum zerstören weiter.

    aber hier sieht man wieder die mächtigkeit des copy&swap idioms. aber wie gesagt, die frage ist einfach falsch, deshalb gibt es auch keine sinnvolle antwort.

    zu deinem komischen code zu meinem serialisierungsbeispiel:
    du gehst davon aus dass man das objekt o nur hier verwendet, was aber wenn man das objekt schon die ganze zeit verwendet und jetzt nur neue settings lädt:

    Klasse o;
    
    //...
     while(foo()) {
       if(load_new_obj) {
         try {
           o=newObj();
         } catch(...) {
           may_or_may_not_be_reached();
         }
       }
       o.something();
    }
    

    natürlich kannst du, wie bei deinem state-beispiel immer den op= atomic machen und somit das copy&swap aus dem op= herausziehen.

    das funktioniert natürlich auch immer:

    Klasse* o = new Klasse();
    
    //...
     while(foo()) {
       if(load_new_obj) {
         try {
           Klasse* old=o;
           o=newObj();
           delete old;
         } catch(...) {
           may_or_may_not_be_reached();
         }
       }
       o.something();
    }
    

    damit verschiebst du die copy&swap stelle einfach auf eine andere ebene. das konzept bleibt gleich.



  • pumuckl schrieb:

    In den Beiträgen weiter oben gings um die algorithmen, z.B. std::copy,

    Ja, das war das Problem. Exceptionsicherheit im Sinne von "keine halben Sachen" paßt gut auf Objekte, aber nicht auf Algorithmen, deshalb war alles zu std::copy zu schräg.



  • Shade Of Mine schrieb:

    antenna schrieb:

    Gerade das, was es nicht kann, nämlich den ganzen Rollback, wäre das interessante. Und zu solchen Libraries ahb ich ein paar Zeilen weiter unten schon was gesagt.

    trivial:

    template<typename InIter, typename OutIter>
    void rollback_copy(InIter begin, InIter end, OutIter out) {
      std::vector<typename iterator_traits<InIter>::value_type> temp(begin, end);
      InIter i=temp.begin();
      InIter e=temp.end();
      while(i!=e) {
        swap(*i, *out);
        ++i; ++out;
      }
    }
    

    verlangt natürlich eine menge sachen und ist deshalb nicht wirklich so toll. denn out muss zB auf genug fertig konstruierte objekte zeigen. dass muss es bei copy nicht (man kann ja nen inserter verwenden oder ähnliches). aber genau so geht ein rollback. natürlich verwendet diese funktion das objekt in dessen op= die exception geflogen ist nur zum zerstören weiter.

    aber hier sieht man wieder die mächtigkeit des copy&swap idioms. aber wie gesagt, die frage ist einfach falsch, deshalb gibt es auch keine sinnvolle antwort.

    Das es sinnvolle Anwendungen von Copy&Swap oder leichten Variationen davon gibt, hab ich auch nie bestritten, nur eben die Anwendungsfälle wo man exceptionsichere assignment Operatoren braucht, finde ich relativ unrealistisch, wenn man nicht gerade sowas wie standard libraries programmiert die für alle theoretischen Fälle funktionieren müssen.

    Ist iterator !=, ++ und * eigentlich immer non-throw? Ich bin mir jetzt nicht sicher, wie das umgesetzt ist, aber kann bei *i oder *out nicht auch der Speicher ausgehen? Dann wäre dein rollback_copy nicht sicher.



  • antenna schrieb:

    Das es sinnvolle Anwendungen von Copy&Swap oder leichten Variationen davon gibt, hab ich auch nie bestritten, nur eben die Anwendungsfälle wo man exceptionsichere assignment Operatoren braucht, finde ich relativ unrealistisch, wenn man nicht gerade sowas wie standard libraries programmiert die für alle theoretischen Fälle funktionieren müssen.

    Du hast sehr komische Vorstellungen... Ob du eine Library oder eine Anwendung programmierst ist von der implementierung her egal. Da jede Anwendung ja ein gewisses Framework bereitstellen muss damit sie funktionieren kann.

    Und von welcher Funktion wir reden ist auch komplett egal - denn entweder sind alle funktionen exception sicher oder garkeine.

    Ist iterator !=, ++ und * eigentlich immer non-throw? Ich bin mir jetzt nicht sicher, wie das umgesetzt ist, aber kann bei *i oder *out nicht auch der Speicher ausgehen? Dann wäre dein rollback_copy nicht sicher.

    wenn einer dieser operatoren wirft, dann hat irgendwer ordentlich mist gebaut. da hier nirgendwo kopien, allokationen oder aehnliches stattfinden.



  • antenna schrieb:

    Ist iterator !=, ++ und * eigentlich immer non-throw?

    Nicht grundsätzlich, nein. Da jeder eigene Iteratoren implementieren kann/darf, könnte sich durchaus jemand überlegen in bestimmten Fällen bei diesen Operatoren Exceptions zu werfen. Beispiele:

    op!= könnte werfen wenn die beiden verglichenen Iteratoren nicht in die selbe Sequenz zeigen. Fragwürdig, aber möglich.
    op++ könnte werfen, wenn man den von end() zurückgegebenen Iterator so implementiert, dass er eine spezielle Form hat die nicht inkrementiert werden kann. Durchaus möglich. op* könnte werfen wenn der Iterator auf kein wirkliches Element zeigt (z.B. end()).

    Da sich shade aber bei den Typen der beiden Iteratoren in seinem Algorithmus eh vertan hat (die sind nicht InIter sondern std::vector</.../>::iterator), hat man ein besser bekanntes Verhalten der Iteratoren, außerdem sollte das Werfen nur geschehen wenn jemand beid er Verwendung mist baut (eben ne Schleife von iter1 nach iter2, die in zwei verschiedene Sequenzen zeigen, so dass != keinen Sinn macht, ++ über das Ende hinaus iteriert und * irgendwann Müll dereferenziert). Die Verwendung ist aber hier völlig in Ordnung, so dass da nie was schieflaufen kann.

    Weitere kleine Diskussionen zum Thema Exception safety:
    http://www.gotw.ca/gotw/061.htm
    http://www.gotw.ca/gotw/082.htm


Anmelden zum Antworten