Modernes Exception-Handling Teil 2 - Hinter den Kulissen



  • Ziemlich verständlich und verdammt interessant! Danke dir für den Blick hinter den Vorhang! 🙂



  • Ein paar kleine Anmerkungen:

    Ich kenne kein std::construct oder std::destroy. Als Referenz könnte man diese Implementation zu benutzen:

    template <typename T>
    void construct(T* where, const T& value)
    {
        new (static_cast<void*>(where)) T(value);
    }
    
    template <typename T>
    void destroy(T* where)
    {
        where->~T();
    }
    
    template <typename BidirectionalIterator>
    void destroy(BidirectionalIterator first, BidirectionalIterator last)
    {
        typedef typename std::iterator_traits< BidirectionalIterator >::value_type value_type;
        for ( ; last != first; )
            (--last)->~value_type();
    }
    

    sizeof benötigt Klammern, wenn der Operand ein Typ ist. Fehlplatziertes const im Zuweisungsoperator.

    Die Verteilung der Aufgaben auf Stack und StackImpl ist verwirrend, da Stack ist für die Erzeugung und StackImpl für die Zerstörung der Objekte zuständig ist. Sinnvoller und einfacher wäre die Verwendung einer einfachen RAII-Klasse, die nur den rohen Speicher als solchen verwaltet. Tatsächlich bietet sich hier ein Smartpointer an - wenn man will, z.B. auch shared_array mit custom-deleter. Dann muss man auch nicht in den Innereien anderer lassen herumpfuschen.

    Ein pop(), das den gepopten Wert by Value liefert, kann nie exceptionsicher sein.

    Hier wage ich zu widersprechen. Es ist lediglich nicht ganz trivial (und grundsätzlich bin ich auch für die AUfteilung in top und pop - denn je nachdem, wie man ein pop mit return implementiert, hat man nur zwei schlechte Alternativen:
    1. wenn das Element trotz Exception entfernt wird, verliert man unwiederbringlich Daten

    T pop()
    {
        if(empty())
            throw StackEmptyException();
        struct on_exit
        {
            stack& s;
            on_exit(stack& s) : s(s) {}
            ~on_exit() { destroy(s.impl.data+--s.impl.used); }
        } on_exit_guard( *this );
        return impl.data[impl.used-1];
    }
    

    2. wenn der Stack bei Exception unverändert bleibt, kann man ihn ggf. überhaupt nicht mehr löschen.

    T pop()
    {
        if(empty())
            throw StackEmptyException();
        struct on_exit
        {
            stack& s;
            bool do_pop;
            on_exit(stack& s) : s(s), do_pop(true) {}
            ~on_exit() { if (do_pop) destroy(s.impl.data+--s.impl.used); }
            void dismiss() { do_pop=false; }
        } on_exit_guard( *this );
        try
        {
            return impl.data[impl.used-1];
        }
        catch ( ... )
        {
            on_exit_guard.dismiss();
            throw;
        }
    }
    

  • Mod

    camper schrieb:

    Die Verteilung der Aufgaben auf Stack und StackImpl ist verwirrend, da Stack ist für die Erzeugung und StackImpl für die Zerstörung der Objekte zuständig ist.

    Ist eine gaengige Implementierunsvariante. irgendwas ala shared_X ist einfach nur falsch hier zu verwenden. ein scoped array uU, aber wozu? eine klasse muss nicht immer eine vollstaendig allein funktionierende einheit sein. stackimpl ist nur ein helfer der sich um den speicher kuemmert.

    impl ist eine RAII klasse, nur eben keine selbststaendige. aber wozu muss sie eine vollstaendige eigenstaendige klasse sein? welchen vorteil habe ich davon? mehr aufwand. und wiederverwenden kann ich es eh nicht.



  • Frage zum Artikel, da steht, daß man nicht den ganzen Code in einen try-Block setzen soll. Warum nicht?

    MFG



  • Weil du dann nicht weißt woher und warum die Exception geflogen ist, könnte quasi ja von überall herkommen. Ist aber oft als "last resort" ganz außen in Programmen üblich.

    MfG SideWinder



  • @SideWinder sagte in Modernes Exception-Handling Teil 2 - Hinter den Kulissen:

    Weil du dann nicht weißt woher und warum die Exception geflogen ist, könnte quasi ja von überall herkommen. Ist aber oft als "last resort" ganz außen in Programmen üblich.

    MfG SideWinder

    Im Artikel steht aber auch daß man Exceptions zentral auffangen soll. Das beißt sich schon ein bischen, meinen Sie nicht auch?

    Denn: Eine zentrale Fehlerbehandlung funktioniert ja nur wenn die Exceptions entsprechend klassifiziert sind. Und genau damit weiß man auch wo sie gefallen sind. Noch dazu wenn ein Backtrace verfügbar ist.

    MFG



  • Beides ist imho richtig: fange kontextlose Exceptions möglichst lokal und kontextbehaftete dann zentral. Ich versuch's mal mit einem Beispiel:

    try
    {
        auto x = query_database_a(); // wirft network_exception wenn datenbank nicht erreichbar
    
        // wenn hier drin noch viele andere query_database_b(), etc. wären, die auch network_exception werfen, wüsste man unten im catch nicht welche datenbank überhaupt die war die nicht erreichbar war => daher möglichst kleiner try/catch-block
    }
    catch(network_exception& ne)
    {
        // network_exception ist kontextlos, aber weil ich try nur um ein statement rumgemacht habe, weiß ich sofort, dass es sich um ein problem bei query_database_a() handeln muss:
        throw database_a_error("Could not reach database A."); // diese exception hier kann man nun möglichst zentral catchen und einen error dialog anzeigen oder ähnliches
    }
    

    Hoffe das Beispile hilft. Ist zumindest ein Weg den ich in C#/Java immer gehe, kenne mich mit aktuellem C++ Exception Best Practices nicht aus.

    MfG SideWinder



  • @SideWinder

    ich hatte mal ein Programm (Perl) das lief nicht auf jedem System. Selbst der Kollege der den Code entwickelt hatte wusste nicht warum, außer der Feststellung daß es wohl ein lokales Problem sein müsse.
    Kurzerhand setzte ich den ganzen Code in einen try-Block und fand damit raus woran es gelegen hat. Natürlich war es ein lokales Problem aber das war ja vorher schon klar wenns bei allen anderen Kunden läuft nur bei dem einem nicht.

    MFG



  • @SideWinder

    zu Deinem Code: Wenn eine network_exception von der Stange nicht unterscheidet zwischen einer nicht zustandegekommenen Verbindung und einer verlorenen Verbindung ist das ein Grund über eine eigene Klassifizierung nachzudenken, also eigene Exception-Klassen. darüber hinaus gibt es sicher auch in C# und Java Möglichkeiten vor einer Query den Verbindungsstatus abzufragen (ping).
    Auf diese Art und Weise muss man nicht jedes Statement in einen eigenen try-Block setzen sondern einfach nur eine gezielte Ex werfen die zentral gefangen wird.

    MFG



  • Auch wenn eine sehr gute Exception geworfen wird, wird sie evtl. nicht zwischen database_a und database_b unterscheiden wenn es zwei Connections sind die zurselben Art von DB gehen, aber sind natürlich alles konstruierte Beispiele, wenn die Exception gut genug ist, spricht natürlich absolut nichts dagegen den catch-Block so weit außen wie möglich zu machen.

    MfG SideWinder



  • @SideWinder

    so ist es. Es kommt ja auch darauf an, was mit der Exception gemacht werden soll. Ich hatte z.B. mal ein Programm für einen Datentransfer von Oracle nach MySQL zu entwickeln, da kam es nur darauf an, daß dieses Programm bei nicht Erreichbarkeit einer der beiden (egal welche) in einer Warteschleife weiterläuft bis die Verbindung wieder steht.

    Schönen Sonntag!



  • Die Aussage ist wohl nur, wenn du innerhalb einer Methode einen try/catch-Block machst, dann ist es best practice ihn so klein wie möglich zu machen. Dass du weit außen ein einziges try/catch rund um app.Run() hast, ist davon nicht betroffen.

    MfG SideWinder


  • Gesperrt

    Ich fahre da zwei Philosophien (je nach Projekt)... Entweder, alles "nach oben" weitergeben - oder aber die try-catch-Blöcke so früh und klein wie möglich wählen, damit man a weiß, was die Exception verursachte, und b ggf. darauf entsprechend regieren kann, ohne das Programm zu beenden.

    Ein von @_ro_ro vorgeschlagener Mischmasch führt meines Erachtens nur zu Chaos.



  • @omggg sagte in Modernes Exception-Handling Teil 2 - Hinter den Kulissen:

    Ich fahre da zwei Philosophien (je nach Projekt)... Entweder, alles "nach oben" weitergeben - oder aber die try-catch-Blöcke so früh und klein wie möglich wählen, damit mit man a weiß, was die Exception verursachte, und b ggf. darauf entsprechend regieren kann, ohne das Programm zu beenden.

    Ein von @_ro_ro vorgeschlagener Mischmasch für meines Erachtens nur zu Chaos.

    Ich habe gar nichts vorgeschlagen. Ich finde nur, daß der Artikel in sich widersprüchliche Aussagen trifft.

    Im übrigen ist meine Frage immer noch nicht beantwortet. MFG


  • Gesperrt

    @_ro_ro sagte in Modernes Exception-Handling Teil 2 - Hinter den Kulissen:

    @omggg sagte in Modernes Exception-Handling Teil 2 - Hinter den Kulissen:

    Ich fahre da zwei Philosophien (je nach Projekt)... Entweder, alles "nach oben" weitergeben - oder aber die try-catch-Blöcke so früh und klein wie möglich wählen, damit mit man a weiß, was die Exception verursachte, und b ggf. darauf entsprechend regieren kann, ohne das Programm zu beenden.

    Ein von @_ro_ro vorgeschlagener Mischmasch für meines Erachtens nur zu Chaos.

    Ich habe gar nichts vorgeschlagen. Ich finde nur, daß der Artikel in sich widersprüchliche Aussagen trifft.

    Im übrigen ist meine Frage immer noch nicht beantwortet. MFG

    Kein Grund, aggressiv zu werden oder zu eskalieren ... Ich habe nur den Subtext gedeutet.



  • @_ro_ro sagte in Modernes Exception-Handling Teil 2 - Hinter den Kulissen:

    Frage zum Artikel, da steht, daß man nicht den ganzen Code in einen try-Block setzen soll. Warum nicht?

    @_ro_ro sagte in Modernes Exception-Handling Teil 2 - Hinter den Kulissen:

    Ich habe gar nichts vorgeschlagen. Ich finde nur, daß der Artikel in sich widersprüchliche Aussagen trifft.

    Welche Sätze meinst du denn genau? Zumindestens in diesem (Teil-)Artikel finde ich keine Aussagen bzgl. der Vermeidung eines globalen try...catch-Handlers.



  • Da der Thread bzw der Artikel ursprünglich mal "Modernes Exception Handling" hieß; in modernem C++ gibt es auch andere Möglichkeiten der Fehlerbehandlung, z.B. mit std::optionaloder noch neuer std::except. Hier ein Artikel dazu: https://devblogs.microsoft.com/cppblog/cpp23s-optional-and-expected/



  • @Schlangenmensch

    die Erfahrung daß man mit Exceptions seine Fehlerbehandlung vereinfachen kann teile ich auf jeden Fall. Darüber habe ich auch schon vor ein paar Jahren Artikel geschrieben.

    MFG



  • @omggg Es gibt kein Modernes Exception Handling, da keine Ausnahmen stattfinden. Wenn doch ist was anderes unbrauchbar.


Anmelden zum Antworten