Ruft longjmp in C++ Destruktoren auf?



  • "has undefined behaviour" ist doch hinreichend eindeutig, oder nicht?


  • Administrator

    Bashar schrieb:

    "has undefined behaviour" ist doch hinreichend eindeutig, oder nicht?

    Ich bin mir allerdings nicht sicher, ob ich den Teil davor richtig verstanden haben. Deshalb schrieb ich auch, dass ich daraus nicht ganz schlau geworden bin. Wenn meine Hirnzellen das richtig verarbeiten, dann steht dort, dass wenn automatische Objekte zerstört werden, weil eine Exception von A nach B geflogen ist, dann ist es undefiniert, ob diese auch zerstört werden, wenn ein longjmp von A nach B durchgeführt wird.

    (Vielleicht hoffen meine Hirnzellen auch nur einfach, dass sie dies falsch verarbeiten :D)

    Grüssli



  • Nicht ganz. Es nicht nicht undefiniert, ob die Objekte zerstört werden. Das ganze Verhalten ist undefiniert.
    Spekulation: Wahrscheinlich hätte man sagen können, dass es unspezifiziert ist, ob die Objekte zerstört werden. Oder sogar, dass sie nicht zerstört werden. Da in solchen Fällen allerdings das ganze Lebenszyklus-Modell eines Objektes zusammenbricht dürfte man sich auf die sichere Ebene "undefiniert" (sprich: longjmp nur in reinem C bitte, wenn es gar nicht anders geht) geeinigt haben.


  • Mod

    Dravere schrieb:

    Bashar schrieb:

    "has undefined behaviour" ist doch hinreichend eindeutig, oder nicht?

    Ich bin mir allerdings nicht sicher, ob ich den Teil davor richtig verstanden haben. Deshalb schrieb ich auch, dass ich daraus nicht ganz schlau geworden bin. Wenn meine Hirnzellen das richtig verarbeiten, dann steht dort, dass wenn automatische Objekte zerstört werden, weil eine Exception von A nach B geflogen ist, dann ist es undefiniert, ob diese auch zerstört werden, wenn ein longjmp von A nach B durchgeführt wird.

    (Vielleicht hoffen meine Hirnzellen auch nur einfach, dass sie dies falsch verarbeiten :D)

    Grüssli

    fast. Dort steht, dass wenn automatische Objekte zerstört werden würden (d.h. nicht-PODs, denn PODs werden nicht zerstört sondern nur freigegeben), falls eine Exception per throw von A nach B fliegen würde, dann löst die Verwendung von longjmp zum gleichen Zielpunkt B an Stelle des throws undefiniertes Verhalten aus.
    Es steht nicht da, dass Destruktoren nicht ausgeführt werden: das würde nämlich das Verhalten spezifizieren und gerade nicht undefiniert lassen.

    Das ganze ist deshalb so umständlich formuliert, weil longjmp natürlich immer noch dort funktionieren soll, wo im Grunde nur reines C verwendet wird.


  • Administrator

    Danke, bestätigt somit meine Befürchtungen endgültig.

    Mal sehen, wie ich das nun am besten löse...

    Grüssli



  • Wahrscheinlich musst du an den kritischen Stellen (die Zeit, in der das böse longjmp() aufgerufen werden könnte) auf reines C zurückgreifen, zumindest was die Objektsemantik betrifft. Andere C++-Features wie Templates kannst du ja nach wie vor verwenden. Vielleicht besteht eine Möglichkeit darin, C++-Objekte mit einem C-Interface zu wrappen, wie es in Bindings getan wird. Dann musst du die Wrapper-Objekte zwar manuell konstruieren und zerstören, aber kannst intern normal arbeiten.

    Das Problem ist aber nicht nur ein C++-spezifisches. Wie löst man es in C, wenn eine aufgerufene Funktion den Programmkontext neu setzen kann? Falls die Kontrolle nicht direkt zum Aufrufer zurückgelangt, wird es mit Speicherfreigabe etc. unter Umständen schwierig. Überhaupt stelle ich mir das Arbeiten mit setjmp() und longjmp() ziemlich übel vor...


  • Administrator

    Nexus schrieb:

    Wahrscheinlich musst du an den kritischen Stellen (die Zeit, in der das böse longjmp() aufgerufen werden könnte) auf reines C zurückgreifen, zumindest was die Objektsemantik betrifft. Andere C++-Features wie Templates kannst du ja nach wie vor verwenden. Vielleicht besteht eine Möglichkeit darin, C++-Objekte mit einem C-Interface zu wrappen, wie es in Bindings getan wird. Dann musst du die Wrapper-Objekte zwar manuell konstruieren und zerstören, aber kannst intern normal arbeiten.

    Aktuell sieht es so aus, dass ich womöglich folgendes machen kann:

    begin callback
    
     some longjmp danger code
    
     normal c++ code without longjmp
    
     some longjmp danger code
    
    end callback
    

    So könnte ich eine Funktion aufrufen, welche dann den C++ Code enthält oder halt einfach einen zusätzlichen Block einführen.

    Nexus schrieb:

    Das Problem ist aber nicht nur ein C++-spezifisches. Wie löst man es in C, wenn eine aufgerufene Funktion den Programmkontext neu setzen kann? Falls die Kontrolle nicht direkt zum Aufrufer zurückgelangt, wird es mit Speicherfreigabe etc. unter Umständen schwierig. Überhaupt stelle ich mir das Arbeiten mit setjmp() und longjmp() ziemlich übel vor...

    Naja, grundsätzlich gleich wie mit Exceptions. Gewisse Kompiler verwenden setjmp und longjmp um Exceptions umzusetzen. In C muss man halt viel Code selber hinschreiben, welcher der Kompiler in C++ hinschreiben würde. In C besteht somit die Gefahr, dass man was vergisst. Man kann da auch mit Makros rumtricksen, aber auch da muss man dann auf gewisse Dinge achten. Möglich ist es definitiv und kann unter Umständen auch die Fehlerbehandlung etwas erleichtern. Also ellenlange if-else Prüfungen vernichten.

    Aber es ist definitiv Vorsicht geboten 🙂

    Grüssli



  • Dravere schrieb:

    So könnte ich eine Funktion aufrufen, welche dann den C++ Code enthält oder halt einfach einen zusätzlichen Block einführen.

    Ja, das ist natürlich das Einfachste, wenn die bösen Funktionen nicht auf Daten deiner C++-Objekte zugreifen und dabei unerwartete Sprünge aus deren Scope durchführen.

    Dravere schrieb:

    Aber es ist definitiv Vorsicht geboten 🙂

    C++-Exceptions passen vor allem sehr gut zu RAII. Ohne automatische Zerstörung würde man den grossen Vorteil der Exceptions einbüssen, nicht auf jeder Ebene der Aufrufhierarchie Fehlerbehandlung und Aufräumaktionen durchführen zu müssen.

    Aber RAII kann ja longjmp() nicht. Wie kann man also Speicher freigeben, wenn eine Funktion den Scope zu verlassen droht? Einen Callback einrichten und jeweils neu mitgeben? An so eine Situation denke ich:

    // C++
    Obj obj;
    FunktionMitThrow(); // kein Problem
    
    // C
    Obj* obj = CreateObj();
    FunktionMitLangemSprung();
    DestroyObj(obj); // evtl. zu spät
    

    Sorry, falls das etwas OffTopic ist. Vielleicht sollte ich die Frage auch im C-Forum stellen... 🙂


  • Administrator

    Nexus schrieb:

    C++-Exceptions passen vor allem sehr gut zu RAII. Ohne automatische Zerstörung würde man den grossen Vorteil der Exceptions einbüssen, nicht auf jeder Ebene der Aufrufhierarchie Fehlerbehandlung und Aufräumaktionen durchführen zu müssen.

    Aber RAII kann ja longjmp() nicht. Wie kann man also Speicher freigeben, wenn eine Funktion den Scope zu verlassen droht? Einen Callback einrichten und jeweils neu mitgeben? An so eine Situation denke ich:

    // C++
    Obj obj;
    FunktionMitThrow(); // kein Problem
    
    // C
    Obj* obj = CreateObj();
    FunktionMitLangemSprung();
    DestroyObj(obj); // evtl. zu spät
    

    Sorry, falls das etwas OffTopic ist. Vielleicht sollte ich die Frage auch im C-Forum stellen... 🙂

    Du kannst dies grundsätzlich einfach über ein künstliches finally erreichen. Du musst dazu natürlich jmp_buf kapseln und immer wenn du ein neuer jmp_buf einführst, musst du den vorherigen speichern. Also grundsätzlich einen zusätzlichen Stack mitführen.

    Also etwas pseudomässig:

    int doRethrow = 0;
    Obj* obj = CreateObj();
    
    push_jmp_frame(); // thread_jmp_frame verweist auf ein neues Objekt.
    
    if(setjmp(thread_jmp_frame->buf) == 0)
    {
      // try
      FunktionMitLangemSprung();
    }
    else
    {
      // catch
      doRethrow = 1;
    }
    
    pop_jmp_frame();
    
    // finally
    DestroyObj(obj);
    
    if(doRethrow)
    {
      rethrow();
    }
    

    Irgendetwas in der Art.

    Grüssli



  • Ah, klingt interessant, danke. Mit Makros könnte man das sicher noch "benutzerfreundlich" machen. 😉

    Ich sehe schon, ich muss wieder mal in Ruhe experimentieren... 🕶


  • Administrator

    Nexus schrieb:

    Ah, klingt interessant, danke. Mit Makros könnte man das sicher noch "benutzerfreundlich" machen. 😉

    Jein, darüber kann man ziemlich streiten. Ich habe bis heute keine Implementation gesehen, wo du dann im TRY-CATCH-FINALLY Bereich einfach ein return hinsetzen konntest. Wegen den Makros wird dies dann sogar versteckt und es steht nur in der Dokumentation, dass dies nicht geht. Das ist natürlich sehr gefährlich, so hast du plötzlich einen Frame nicht entfernt. Dieser Fehler fällt womöglich auch nicht sofort auf, bis irgendwann ein longjmp bis an den Anfang zurückgehen soll. Der Fehler bleibt also vielleicht Jahre lang unentdeckt, die Software läuft Jahre lang ... und dann plötzlich passiert ein äusserst seltsamer Fehler, welche garantiertes undefiniertes Verhalten auslöst und die ganze angeschlosssene Datenbank löscht - Licht aus 🤡

    Also mal ganz unter uns C++'ler:
    Bin ich froh Exceptions und RAII zu haben! 😃

    Grüssli


Anmelden zum Antworten