8 Stimmen

Ist es jemals nicht sicher, eine Ausnahme in einem Konstruktor auszulösen?

Ich weiß, dass es nicht sicher ist, Ausnahmen von Destruktoren zu werfen, aber ist es jemals unsicher, Ausnahmen von Konstruktoren zu werfen?

Was geschieht z. B. bei Objekten, die global deklariert werden? Ein kurzer Test mit gcc und ich bekomme einen Abbruch, ist das immer garantiert? Welche Lösung würden Sie verwenden, um diese Situation zu bewältigen?

Gibt es Situationen, in denen Konstruktoren Ausnahmen auslösen können und die Dinge nicht so lassen, wie wir sie erwarten.

EDIT: Ich denke, ich sollte hinzufügen, dass ich versuche zu verstehen, unter welchen Umständen ich ein Ressourcenleck bekommen könnte. Sieht aus wie die vernünftige Sache zu tun ist, manuell freigeben Ressourcen, die wir erhalten haben, Teil Weg durch den Bau vor dem Auslösen der Ausnahme. Ich habe nie benötigt, um Ausnahmen in Konstruktoren vor heute zu werfen, so versuchen zu verstehen, wenn es irgendwelche Fallstricke gibt.

d.h. ist dies auch sicher?

class P{
  public:
    P() { 
       // do stuff...

       if (error)
          throw exception
    }
}

dostuff(P *p){
 // do something with P
}

... 
try {
  dostuff(new P())
} catch(exception) {

}

wird der dem Objekt P zugewiesene Speicher freigegeben?

EDIT2: Ich habe vergessen zu erwähnen, dass dostuff in diesem speziellen Fall den Verweis auf P in einer Ausgabewarteschlange speichert. P ist eigentlich eine Nachricht, und dostuff nimmt die Nachricht, leitet sie an die entsprechende Ausgabewarteschlange weiter und sendet sie. Im Grunde genommen wird die Nachricht, sobald sie in den Händen von dostuff ist, später in den Innereien von dostuff wieder freigegeben. Ich denke, ich möchte ein autoptr um P legen und release auf dem autoptr nach dostuff aufrufen, um ein Speicherleck zu verhindern, wäre das korrekt?

30voto

GManNickG Punkte 476445

Das Auslösen von Ausnahmen in einem Konstruktor ist eine gute Sache . Wenn etwas in einem Konstruktor fehlschlägt, haben Sie zwei Möglichkeiten:

  • einen "Zombie"-Status beibehalten, in dem die Klasse zwar existiert, aber nichts tut, oder
  • Eine Ausnahme auslösen.

Und die Aufrechterhaltung von Zombie-Klassen kann ziemlich mühsam sein, wenn die eigentliche Antwort lauten sollte: "Das ist fehlgeschlagen, was nun?".

Gemäß der Norm unter 3.6.2.4:

Wenn die Konstruktion oder Zerstörung eines nichtlokalen statischen Objekts mit einer nicht abgefangenen Ausnahme endet, ist das Ergebnis ein Aufruf von terminate (18.6.3.3).

Wobei sich terminieren auf std::terminate .


In Bezug auf Ihr Beispiel: nein. Das liegt daran, dass Sie nicht mit RAII-Konzepte . Wenn eine Ausnahme ausgelöst wird, wird der Stapel abgewickelt, was bedeutet, dass alle Objekte ihre Destruktoren aufgerufen bekommen, wenn der Code zur nächsten entsprechenden catch Klausel.

Ein Zeiger hat keinen Destruktor. Lassen Sie uns einen einfachen Testfall machen:

#include <string>

int main(void)
{
    try
    {
        std::string str = "Blah.";
        int *pi = new int;

        throw;

        delete pi; // cannot be reached
    }
    catch(...)
    {
    }
}

Hier, str wird Speicher zugewiesen und "Blah." dorthin kopiert, und pi wird so initialisiert, dass es auf eine Ganzzahl im Speicher zeigt.

Wenn eine Ausnahme ausgelöst wird, beginnt das Abwickeln des Stapels. Zuerst wird der Destruktor des Zeigers "aufgerufen" (nichts getan), dann str Destruktor, der den ihm zugewiesenen Speicher freigibt.

Wenn Sie RAII-Konzepte verwenden, würden Sie einen intelligenten Zeiger benutzen:

#include <memory>
#include <string>

int main(void)
{
    try
    {
        std::string s = "Blah.";
        std::auto_ptr<int> pi(new int);

        throw;

        // no need to manually delete.
    }
    catch(...)
    {
    }
}

Hier, pi ruft der Destruktor von delete und es wird kein Speicherplatz verloren gehen. Aus diesem Grund sollten Sie Ihre Zeiger immer umhüllen, und das ist auch der Grund, warum wir std::vector anstatt Zeiger manuell zuzuweisen, ihre Größe zu ändern und sie wieder freizugeben. (Sauberkeit und Sicherheit)

Editar

Ich vergaß zu erwähnen. Sie haben das gefragt:

Ich denke, ich möchte ein Autoptr um P setzen und Release auf dem Autoptr nach dostuff aufrufen, um ein Speicherleck zu verhindern, wäre das richtig?

Ich habe es nicht ausdrücklich gesagt, sondern nur angedeutet, aber die Antwort lautet pas de . Alles, was Sie tun müssen, ist, ihn in den auto_ptr und wenn die Zeit gekommen ist, wird sie automatisch gelöscht. Das manuelle Freigeben macht den Zweck der Ablage in einem Container zunichte.

Ich würde auch vorschlagen, dass Sie sich mit fortgeschritteneren intelligenten Zeigern befassen, wie z. B. mit denen in erhöhen . Ein außerordentlich beliebtes Beispiel ist shared_ptr das ist Referenz gezählt Dadurch eignet es sich zur Lagerung in Containern und zum Kopieren. (Im Gegensatz zu auto_ptr . Do pas verwenden. auto_ptr in Containern!)

8voto

Michael Burr Punkte 320591

Als Spence erwähnt Wenn eine Ausnahme aus einem Konstruktor geworfen wird (oder eine Ausnahme aus einem Konstruktor entweichen kann), besteht die Gefahr, dass Ressourcen verloren gehen, wenn der Konstruktor nicht sorgfältig geschrieben ist, um diesen Fall zu behandeln.

Dies ist ein wichtiger Grund, warum die Verwendung von RAII-Objekten (wie z.B. Smart Pointer) bevorzugt werden sollte - sie übernehmen automatisch die Bereinigung von Ausnahmen.

Wenn Sie Ressourcen haben, die gelöscht oder anderweitig manuell freigegeben werden müssen, müssen Sie dafür sorgen, dass diese bereinigt werden, bevor die Ausnahme das System verlässt. Dies ist nicht immer so einfach, wie es sich anhört (und sicherlich nicht so einfach, wie wenn ein RAII-Objekt dies automatisch erledigen würde).

Und vergessen Sie nicht: Wenn Sie etwas, das in der Initialisierungsliste des Konstruktors passiert, manuell bereinigen müssen, müssen Sie die funky 'function-try-block'-Syntax verwenden:

C::C(int ii, double id)
try
     : i(f(ii)), d(id)
{
     //constructor function body
}
catch (...)
{
     //handles exceptions thrown from the ctor-initializer
     //and from the constructor function body
}

Denken Sie auch daran, dass die Ausnahmesicherheit der wichtigste (einzige??) Grund dafür ist, dass das 'Swap'-Idiom weit verbreitet ist - es ist ein einfacher Weg, um sicherzustellen, dass Kopierkonstruktoren keine Objekte angesichts von Ausnahmen auslaufen lassen oder beschädigen.

Das Fazit ist also, dass die Verwendung von Ausnahmen zur Behandlung von Fehlern in Konstruktoren zwar in Ordnung ist, aber nicht unbedingt automatisch erfolgt.

5voto

Martin York Punkte 245363

Wenn Sie eine Ausnahme von einem Konstruktor auslösen, passieren einige Dinge.

1) Bei allen vollständig konstruierten Mitgliedern werden ihre Destruktoren aufgerufen.
2) Der für das Objekt zugewiesene Speicher wird freigegeben.

Um die Dinge zu automatisieren, sollten Sie keine RAW-Zeiger in Ihrer Klasse haben. Einer der Standard-Smart-Pointer reicht in der Regel aus, und die Compiler-Optimierung reduziert den Großteil des Overheads auf praktisch nichts [oder nur die Arbeit, die Sie ohnehin manuell hätten erledigen müssen].

Übergabe von Zeigern an Funktionen

Die andere Sache, die ich NICHT tun würde, ist ein Wert an eine Funktion als Zeiger übergeben.
Das Problem dabei ist, dass Sie nicht angeben, wem das Objekt gehört. Ohne die implizite Eigentümerinformation ist (wie bei allen C-Funktionen) unklar, wer für das Aufräumen des Zeigers verantwortlich ist.

dostuff(P *p)
{
    // do something with P
}

Sie erwähnen, dass p in einer Warteschlange gespeichert und zu einem späteren Zeitpunkt verwendet wird. Dies impliziert, dass Sie das Eigentum an dem Objekt an die Funktion übergeben. Machen Sie also diese Beziehung explizit, indem Sie std::auto_ptr verwenden. Dadurch weiß der Aufrufer von dostuff(), dass er den Zeiger nach dem Aufruf von dostuff() nicht mehr verwenden kann, da durch den Aufruf der Funktion der Zeiger tatsächlich in die Funktion übertragen wurde (d.h. der lokale auto_ptr des Aufrufers enthält nach dem Aufruf von dostuff() einen NULL-Zeiger).

void doStuff(std::auto_ptr<P> p)
{
    // do something with p
    //
    // Even if you do nothing with the RAW pointer you have taken the
    // pointer from the caller. If you do not use p then it will be auto
    // deleted.
}

int main()
{
    // This works fine.
    dostuff(std::auto_ptr<P>(new P));

    // This works just as well.
    std::auto_ptr<P>    value(new P);
    dostuff(value);

    // Here you are guranteed that value has a NULL pointer.
    // Because dostuff() took ownership (and physically took)
    // of the pointer so it could manage the lifespan.
}

Speicherung von Zeigern in einem Cointainer

Sie erwähnen, dass dostuff() verwendet wird, um eine Liste von p-Objekten für die spätere Verarbeitung zu speichern.
Das bedeutet also, dass Sie die Objekte in einen Container stecken. Nun unterstützen die normalen Container nicht std::auto_ptr. Aber boost unterstützt Container mit Zeigern (wo der Container das Eigentum übernimmt). Auch diese Container verstehen auto_ptr und übertragen automatisch den Besitz von auto_ptr auf den Container.

boost::ptr_list<P>   messages;
void doStuff(std::auto_ptr<P> p)
{
    messages.push_front(p);
}

Beachten Sie, dass beim Zugriff auf Mitglieder dieser Container immer ein Verweis (kein Zeiger) auf das enthaltene Objekt zurückgegeben wird. Dies zeigt an, dass die Lebensdauer des Objekts an die Lebensdauer des Containers gebunden ist und der Verweis so lange gültig ist, wie der Container gültig ist (es sei denn, Sie entfernen das Objekt ausdrücklich).

3voto

Dan Breslau Punkte 11294

Die bisherigen Antworten waren ausgezeichnet. Ich möchte nur eine Sache hinzufügen, die sich auf die Antworten von Martin York und Michael Burr bezieht.

Unter Verwendung des Beispielkonstruktors von Michael Burr habe ich eine Zuweisung in den Konstruktorkörper eingefügt:

C::C(int ii, double id)
try
     : i(f(ii)), d(id)
{
     //constructor function body

     d = sqrt(d);

}
catch (...)
{
     //handles exceptions thrown from the ctor-initializer
     //and from the constructor function body
}

Die Frage ist nun, Wann ist d als 'vollständig konstruiert' betrachtet so dass sein Destruktor aufgerufen wird, wenn im Konstruktor eine Ausnahme ausgelöst wird (wie in Martins Beitrag)? Die Antwort ist: nach dem Initialisierer

 : i(f(ii)), d(id)

Der Punkt ist, dass die Felder Ihres Objekts toujours deren Destruktoren aufgerufen werden, wenn im Konstruktor eine Ausnahme ausgelöst wird Körper . (Dies gilt unabhängig davon, ob Sie tatsächlich Initialisierungen für diese Felder angegeben haben oder nicht). Umgekehrt, wenn eine Ausnahme vom Initialisierer eines anderen Feldes ausgelöst wird, dann werden Destruktoren se für die Felder aufgerufen werden, deren Initialisierer bereits gelaufen sind (und seulement für diese Felder).

Dies impliziert, dass es die beste Praxis ist, keinem Feld zu erlauben, den Konstruktorkörper mit einem unzerstörbaren Wert (z.B. einem undefinierten Zeiger) zu erreichen. Da dies der Fall ist, ist es am besten, den Feldern ihre realen Werte durch die Initialisierer zu geben, anstatt (z.B.) zuerst die Zeiger auf NULL zu setzen und ihnen dann ihre 'realen' Werte im Konstruktorkörper zu geben.

0voto

Spence Punkte 27536

Wenn Sie eine Ressource in einem Konstruktor, wie z. B. ein Socket usw., dann wäre dies durchgesickert, wenn Sie eine Ausnahme geworfen würde es nicht?

Aber ich schätze, das ist das Argument dafür, keine Arbeit in einem Konstruktor zu machen, sondern die Verbindungen einfach zu initialisieren, wenn man sie braucht.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X