Übergang vom C-„goto“-Fehlerbehandlungsparadigma zum C++-Ausnahmebehandlungsparadigma

Lesezeit: 14 Minuten

Benutzer-Avatar
Wilhelm Grau

Ich bin ein C-Programmierer und lerne C++. In C gibt es eine gemeinsame goto Redewendung, die verwendet wird, um Fehler zu behandeln und eine Funktion sauber zu verlassen. Ich habe diese Ausnahmebehandlung über gelesen trycatch Blöcke werden in objektorientierten Programmen bevorzugt, aber ich habe Probleme, dieses Paradigma in C++ zu implementieren.

Nehmen Sie zum Beispiel die folgende Funktion in C, die die verwendet goto Paradigma der Fehlerbehandlung:

unsigned foobar(void){
    FILE *fp = fopen("blah.txt", "r");
    if(!fp){
        goto exit_fopen;
    }

    /* the blackbox function performs various
     * operations on, and otherwise modifies,
     * the state of external data structures */
    if(blackbox()){
        goto exit_blackbox;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data = malloc(NUM_DATUM*sizeof(*data));
    if(!data){
        goto exit_data;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            goto exit_read;
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        printf("%lu\n", data[i] + data[i + NUM_DATUM/2]);
    }

    free(data)
    /* the undo_blackbox function reverts the
     * changes made by the blackbox function */
    undo_blackbox();
    fclose(fp);

    return 0;

exit_read:
    free(data);
exit_data:
    undo_blackbox();
exit_blackbox:
    fclose(fp);
exit_fopen:
    return 1;
}

Ich habe versucht, die Funktion in C++ mit dem Paradigma der Ausnahmebehandlung als solches neu zu erstellen:

unsigned foobar(){
    ifstream fp ("blah.txt");
    if(!fp.is_open()){
        return 1;
    }

    try{
        // the blackbox function performs various
        // operations on, and otherwise modifies,
        // the state of external data structures
        blackbox();
    }catch(...){
        fp.close();
        return 1;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data;
    try{
        data = new unsigned long [NUM_DATUM];
    }catch(...){
        // the undo_blackbox function reverts the
        // changes made by the blackbox function
        undo_blackbox();
        fp.close();
        return 1;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;
        if(!getline(fp, buffer)){
            delete[] data;
            undo_blackbox();
            fp.close();
            return 1;
        }

        stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    delete[] data;
    undo_blackbox();
    fp.close();

    return 0;
}

Ich habe das Gefühl, dass meine C++-Version das Ausnahmebehandlungsparadigma nicht richtig implementiert hat; Tatsächlich scheint die C++-Version sogar noch weniger lesbar und fehleranfälliger zu sein, da sich ein Build von Bereinigungscode in der ansammelt catch Blöcke, wenn die Funktion wächst.

Ich habe gelesen, dass all dieser Bereinigungscode in den Catch-Blöcken in C++ aufgrund von etwas namens unnötig sein kann RAIIaber ich bin mit dem Konzept nicht vertraut. Ist meine Implementierung richtig oder gibt es eine bessere Möglichkeit, Fehler zu behandeln und eine Funktion in C++ sauber zu beenden?

  • Wenn die Größe von data ist eine kleine Kompilierzeitkonstante, warum weisen Sie sie dynamisch zu? Wenn Sie möchten, verwenden Sie std::vector die nicht explizit gelöscht werden muss, egal wie die Funktion zurückgibt oder auslöst. Sie müssen den Dateistream auch nicht explizit schließen. Setzen Sie Ihre gesamte Funktion in einen try-Block und entscheiden Sie abhängig von einer Logik (einem Flag oder verschiedenen Ausnahmetypen abfangen), ob Sie undo_blackbox aufrufen.

    – Neil Kirk

    20. Februar 2015 um 14:25 Uhr


  • Schauen Sie auf jeden Fall nach RAII. Zum Beispiel Ihre fp.close() ist unnötig. Mit einem std::vector sollte auch Ihre Datenlöschung obsolet werden.

    – nvoigt

    20. Februar 2015 um 14:26 Uhr

  • Der Punkt von RAII ist folgender: Sie müssen nicht anrufen fp.close() Wenn blackbox() wirft (oder überhaupt) – ifstreamDer Destruktor von erledigt dies für Sie. Verwenden ein Zielfernrohrschutz automatisch laufen lassen undo_blackbox() bei anormalem Ausgang. Machen data ein std::vector<unsigned long> – Sein Destruktor gibt automatisch Speicher frei, unabhängig davon, ob die Funktion normal oder über eine Ausnahme beendet wird. Sobald Sie alle Ressourcen über RAII verwalten, schreiben Sie sehr selten try/catch überhaupt blockieren – Sie würden nur die Ausbreitung von Ausnahmen zulassen.

    – Igor Tandetnik

    20. Februar 2015 um 14:27 Uhr


  • Bevor Sie RAII-Muster überall in der Show implementieren, ist es meines Erachtens ratsam, innezuhalten und sich zu fragen: “Ist es vernünftig, diesen Bereinigungscode auszuführen, wenn eine nicht behandelte Ausnahme im Stapel nach oben weitergegeben wird?” Dieses Muster kam mir immer sehr gefährlich vor! Wenn Sie die Ausnahme erwartet haben, würden Sie sie behandeln. Wenn die Ausnahme also nicht behandelt wird, ist sie wahrscheinlich unerwartet Sie kennen den Status Ihres eigenen Programms nicht. Ist es wirklich ratsam, in diesem Szenario eine ganze Menge mehr Code auszuführen? Es ist, als würde beim Mittagessen eine Bombe hochgehen, und bevor Sie das Gebäude verlassen, spülen Sie Ihr Geschirr.

    – Eric Lippert

    20. Februar 2015 um 23:55 Uhr


  • @EricLippert Nun, das einzig Vernünftige ist das Abbrechen … Sie lösen keine Ausnahme wegen eines beschädigten Zustands aus, Sie brechen ab, wenn Sie einen beschädigten Zustand haben. Ausnahmen gelten für einen nicht beschädigten, aber ungültigen Zustand (wie das Zuweisen von Speicher, wenn kein freier Speicher vorhanden ist; nicht wie das Zuweisen von Speicher, wenn die Liste freier Blöcke beschädigt ist).

    – Benutzer253751

    21. Februar 2015 um 8:15 Uhr


Benutzer-Avatar
Mike Seymour

Das Prinzip von RAII besteht darin, dass Sie einen Klassentyp verwenden, um alle Ressourcen zu verwalten, die nach der Verwendung bereinigt werden müssen. diese Bereinigung wird vom Destruktor durchgeführt.

Das bedeutet, dass Sie einen lokalen RAII-Manager erstellen können, der automatisch alles bereinigt, was er verwaltet, wenn es den Bereich verlässt, sei es aufgrund des normalen Programmablaufs oder einer Ausnahme. Es sollte niemals eine Notwendigkeit für eine geben catch blockieren, nur um aufzuräumen; nur wenn Sie die Ausnahme behandeln oder melden müssen.

In Ihrem Fall haben Sie drei Ressourcen:

  • Die Datei fp. ifstream ist schon ein RAII-Typ, also einfach die überflüssigen Aufrufe entfernen fp.close() und alles ist gut.
  • Der zugewiesene Speicher data. Verwenden Sie ein lokales Array, wenn es sich um eine kleine feste Größe handelt (wie dies hier der Fall ist), oder std::vector wenn es dynamisch zugewiesen werden muss; dann weg damit delete.
  • Der Staat eingerichtet von blackbox.

Sie können Ihren eigenen RAII-Wrapper für den Malarkey “Black Box” schreiben:

struct blackbox_guard {
    // Set up the state on construction
    blackbox_guard()  {blackbox();}

    // Restore the state on destruction
    ~blackbox_guard() {undo_blackbox();}

    // Prevent copying per the Rule of Three
    blackbox_guard(blackbox_guard const &) = delete;
    void operator=(blackbox_guard) = delete;
};

Jetzt können Sie Ihren gesamten Fehlerbehandlungscode entfernen. Ich würde einen Fehler durch Ausnahmen (entweder ausgelöst oder zur Ausbreitung zugelassen) und nicht durch einen magischen Rückgabewert anzeigen, indem ich Folgendes gebe:

void foobar(){
    ifstream fp ("blah.txt"); // No need to check now, the first read will fail if not open
    blackbox_guard bb;

    const size_t NUM_DATUM = 42;
    unsigned long data[NUM_DATUM];   // or vector<unsigned long> data(NUM_DATUM);

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;

        // You could avoid this check by setting the file to throw on error
        // fp.exceptions(ios::badbit); or something like that before the loop
        if(!getline(fp, buffer)){
             throw std::runtime_error("Failed to read"); // or whatever
        }

        stringstream(buffer) >> data[i]; // or data[i] = stoul(buffer);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }
}

  • Dies ist zugegebenermaßen das falsche Forum, aber wie funktioniert das Ausnahme-/RAII-Modell in der Praxis in großen Anwendungen? Als ich es das letzte Mal ausprobierte, neigte ich dazu, die impliziten Codepfade zu vergessen und das System nach seltenen Ausnahmen in einen inkonsistenten Zustand zu bringen. Auf der anderen Seite bin ich auch häufig dafür bekannt, dass ich die manuellen Bereinigungs- und Fehlerbedingungstests falsch mache.

    – doynax

    20. Februar 2015 um 15:07 Uhr

  • @doynax: Meiner Erfahrung nach ist es das zuverlässigste Modell für umfangreiche Anwendungen – Sie können eine Ausnahme nicht ignorieren oder vergessen, eine Ressource zu bereinigen. Die impliziten Kontrollpfade sind nur dann ein Problem, wenn Sie RAII von vornherein nicht richtig verwendet haben; das heißt, wenn Sie Typen verwenden, die nicht mindestens a bereitstellen grundlegende Ausnahmegarantie. Verwenden Sie RAII konsequent, und Ausnahmen werden Ihre Freunde sein. Tun Sie es nicht, und ein nicht außergewöhnlicher Fluss (Rückgaben, Unterbrechungen usw.) kann ebenfalls ein Problem sein, wenn auch sichtbarer als Ausnahmen.

    – Mike Seymour

    20. Februar 2015 um 15:13 Uhr


  • @Mgetz: Es ist immer noch die Dreierregel für die Korrektheit (Sie müssen sich nur um die Bewegungssemantik kümmern, wenn Sie sie von der Kopiersemantik unterscheiden möchten). Durch das Löschen der Kopiervorgänge werden auch die Verschiebungsvorgänge gelöscht, sodass mein Wrapper weder kopierbar noch verschiebbar ist.

    – Mike Seymour

    20. Februar 2015 um 15:22 Uhr

  • @ChrisDrew: In der Tat erzwingt RAII nicht die Verwendung von Ausnahmen. Ich bevorzuge Ausnahmen, da Rückgabewerte ignoriert werden können (und meiner Erfahrung nach manchmal werden), was zu subtilen Fehlern führt, die Ausnahmen sofort aufgedeckt hätten; andere haben andere meinungen. Aber die Frage ist, wie man RAII verwendet, nicht, ob Ausnahmen verwendet werden sollen.

    – Mike Seymour

    20. Februar 2015 um 15:29 Uhr


  • @doynax: Ja, du brauchst eine starke Ausnahmegarantie bei Zustandsänderungen. Ich würde empfehlen, den veränderlichen Zustand zu vermeiden, aber wir kommen schon ziemlich weit vom Thema ab.

    – Mike Seymour

    20. Februar 2015 um 15:48 Uhr

Benutzer-Avatar
Angew ist nicht mehr stolz auf SO

Ja, Sie sollten nach Möglichkeit RAII (Resource Acquisition Is Initialisation) verwenden. Es führt zu leicht lesbarem Code und sicher.

Die Kernidee ist, dass Sie während der Initialisierung eines Objekts Ressourcen erwerben und das Objekt so einrichten, dass es die Ressourcen bei seiner Zerstörung korrekt freigibt. Der entscheidende Punkt, warum dies funktioniert, ist das Destruktoren werden normal ausgeführt, wenn der Geltungsbereich über eine Ausnahme verlassen wird.

In Ihrem Fall ist RAII bereits verfügbar und Sie verwenden es einfach nicht. std::ifstream (Ich nehme an, das ist Ihre ifstream bezieht sich auf) schließt tatsächlich auf Zerstörung. Also alle close() ruft an catch kann getrost weggelassen werden und geschieht automatisch – genau das, wofür RAII da ist.

Zum data, sollten Sie auch einen RAII-Wrapper verwenden. Es sind zwei verfügbar: std::unique_ptr<unsigned long[]>und std::vector<unsigned long>. Beide kümmern sich um die Speicherfreigabe in ihren jeweiligen Destruktoren.

Schließlich z blackbox()können Sie selbst einen trivialen RAII-Wrapper erstellen:

struct BlackBoxer
{
  BlackBoxer()
  {
    blackbox();
  }

  ~BlackBoxer()
  {
    undo_blackbox();
  }
};

Wenn Sie diese umschreiben, wird Ihr Code viel einfacher:

unsigned foobar() {
  ifstream fp ("blah.txt");
  if(!fp.is_open()){
    return 1;
  }

  try {
    BlackBoxer b;

    const size_t NUM_DATUM = 42;
    std::vector<unsigned long> data(NUM_DATUM);
    for(size_t i = 0; i < NUM_DATUM; i++){
      string buffer;
      if(!getline(fp, buffer)){
        return 1;
      }

      stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
      cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    return 0;
  } catch (...) {
    return 1;
  }
}

Beachten Sie außerdem, dass Ihre Funktion einen Rückgabewert verwendet, um Erfolg oder Fehler anzuzeigen. Dies kann das sein, was Sie wollen (wenn ein Fehler für diese Funktion “normal” ist) oder nur einen halben Weg darstellen (wenn ein Fehler auch eine Ausnahme sein soll).

Wenn letzteres der Fall ist, ändern Sie einfach die Funktion in voidloswerden trycatch konstruieren und stattdessen eine geeignete Ausnahme auslösen return 1;.

Schließlich, selbst wenn Sie sich entscheiden, den Rückgabewert-Ansatz beizubehalten (der vollkommen gültig ist), sollten Sie die Funktion in Rückgabe ändern boolmit true bedeutet Erfolg. Es ist idiomatischer.

In C gibt es ein allgemeines goto-Idiom, das verwendet wird, um Fehler zu behandeln und die Bereinigung einer Funktion zu beenden. Ich habe gelesen, dass die Ausnahmebehandlung über Try-Catch-Blöcke in objektorientierten Programmen bevorzugt wird.

Das gilt überhaupt nicht für C++.

Aber C++ hat stattdessen deterministische Destruktoren finally Blöcke (die zum Beispiel in Java verwendet werden), und das ist ein Spielwechsler für Fehlerbehandlungscode.

Ich habe gelesen, dass all dieser Bereinigungscode in den Catch-Blöcken in C++ aufgrund von etwas namens RAII möglicherweise unnötig ist.

Ja, in C++ verwenden Sie “RAII”. Was ein schlechter Name für ein großartiges Konzept ist. Der Name ist schlecht, weil er die Betonung auf legt ichInitialisierung (Ressourcenerfassung ist Initialisierung). Das Wichtige an RAII hingegen liegt in Zerstörung. Da der Destruktor eines lokalen Objekts am Ende eines Blocks ausgeführt wird, egal was passiert, seien es vorzeitige Rückgaben oder sogar Ausnahmen, ist dies der perfekte Ort für Code, der Ressourcen freigibt.

aber ich bin mit dem Konzept nicht vertraut.

Nun, für den Anfang können Sie mit der Definition von Wikipedia beginnen:

http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

Oder Sie gehen direkt auf die Website von Bjarne Stroustrup:

http://www.stroustrup.com/bs_faq2.html#endlich

Ich bin sicher, wir würden uns freuen, Fragen zu bestimmten Aspekten der Redewendung oder zu Problemen zu beantworten, auf die Sie bei der Verwendung stoßen 🙂

Ist meine Implementierung richtig oder gibt es eine bessere Möglichkeit, Fehler zu behandeln und eine Funktion in C++ sauber zu beenden?

Ihre Implementierung ist nicht was man von gutem C++-Code erwarten würde.

Hier ist ein Beispiel mit RAII. Es verwendet Ausnahmen, um Fehler zu melden, und Destruktoren, um Aufräumarbeiten durchzuführen.

#include <fstream>
#include <stdexcept>
#include <vector>

// C or low-level functions to be wrapped:
int blackbox();
void undo_blackbox();

// just to be able to compile this example:
FILE *fp;

// The only self-made RAII class we need for this example
struct Blackbox {
    Blackbox() {
        if (!blackbox()) {
            throw std::runtime_error("blackbox failed");
        }
    }

    // Destructor performs cleanup:
    ~Blackbox() {
        undo_blackbox();
    }   
};

void foobar(void){
    // std::ifstream is an implementation of the RAII idiom,
    // because its destructor closes the file:
    std::ifstream is("blah.txt");
    if (!is) {
        throw std::runtime_error("could not open blah.txt");
    }

    Blackbox local_blackbox;

    // std::vector itself is an implementation of the RAII idiom,
    // because its destructor frees any allocated data:
    std::vector<unsigned long> data(42);

    for(size_t i = 0; i < data.size(); i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            throw std::runtime_error("fgets error");
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < (data.size()/2); i++){
        printf("%lu\n", data[i] + data[i + (data.size()/2)]);
    }

    // nothing to do here - the destructors do all the work!
}

Übrigens +1 für den Versuch, ein neues Konzept in einer neuen Sprache zu lernen. Es ist nicht einfach, seine Denkweise in einer anderen Sprache zu ändern! 🙂

  • Könnte off-topic sein, aber ich mochte die Betonung auf das Fehlen von finally in C++ im Vergleich zu Java. Eigentlich ist es offensichtlich, aber ich habe nie darüber nachgedacht. +1 dafür!

    – andree

    6. Februar 2017 um 13:32 Uhr

Benutzer-Avatar
Mötz

Lassen Sie mich das für Sie umschreiben, indem ich das C++-Idiom mit Erklärungen inline zum Code verwende

// void return type, we may no guarantees about exceptions
// this function may throw
void foobar(){
   // the blackbox function performs various
   // operations on, and otherwise modifies,
   // the state of external data structures
   blackbox();

   // scope exit will cleanup blackbox no matter what happens
   // a scope exit like this one should always be used
   // immediately after the resource that it is guarding is
   // taken.
   // but if you find yourself using this in multiple places
   // wrapping blackbox in a dedicated wrapper is a good idea
   BOOST_SCOPE_EXIT[]{
       undo_blackbox();
   }BOOST_SCOPE_EXIT_END


   const size_t NUM_DATUM = 42;
   // using a vector the data will always be freed
   std::vector<unsigned long> data;
   // prevent multiple allocations by reserving what we expect to use
   data.reserve(NUM_DATUM);
   unsigned long d;
   size_t count = 0;
   // never declare things before you're just about to use them
   // doing so means paying no cost for construction and
   // destruction if something above fails
   ifstream fp ("blah.txt");
   // no need for a stringstream we can check to see if the
   // file open succeeded and if the operation succeeded
   // by just getting the truthy answer from the input operation
   while(fp >> d && count < NUM_DATUM)
   {
       // places the item at the back of the vector directly
       // this may also expand the vector but we have already
       // reserved the space so that shouldn't happen
       data.emplace_back(d);
       ++count;
   }

   for(size_t i = 0; i < NUM_DATUM/2; i++){
       cout << data[i] + data[i + NUM_DATUM/2] << endl;
   }
}

Das mächtigste Feature von c++ sind nicht die Klassen, sondern der Destruktor. Der Destruktor ermöglicht das Entladen oder Freigeben von Ressourcen oder Verantwortlichkeiten, wenn der Gültigkeitsbereich verlassen wird. Das bedeutet, dass Sie den Bereinigungscode nicht mehrmals neu schreiben müssen. Außerdem, weil nur konstruierte Objekte zerstört werden können; Wenn Sie nie zu einem Gegenstand gelangen und ihn daher nie bauen, zahlen Sie keine Strafe für die Zerstörung, wenn etwas passiert.

Wenn Sie feststellen, dass Sie Bereinigungscode wiederholen, sollte dies ein Hinweis darauf sein, dass der betreffende Code die Vorteile des Destruktors und von RAII nicht ausnutzt.

1342270cookie-checkÜbergang vom C-„goto“-Fehlerbehandlungsparadigma zum C++-Ausnahmebehandlungsparadigma

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy