Exception im Konstruktor eines gelöschten Objekts



  • Moin,

    folgendes Problem: in einem Konstruktor wird zum Schluss eine Funktion aufgerufen, die unter bestimmten Umständen veranlasst, dass die gerade erstellte Instanz gleich wieder gelöscht wird. Passiert dies, wird eine entsprechende Exception geworfen. Diese wird im Konstruktor nicht gefangen, da der Aufrufer mitbekommen muss, dass die gerade erstellte Instanz nun ungültig ist.
    Das Problem ist, dass die Exception den Konstruktor nicht verlässt, sondern stattdessen zu einem segmentation fault führt. Wahrscheinlich wird durch die Exception versucht, das Objekt wieder zu dekonstruieren obwohl es bereits gelöscht wurde.
    Wie aber sonst soll die instanzerzeugende Funktion erfahren, dass etwas passiert ist? Im Moment habe ich es so gelöst, dass ich eine create-Funktion habe, die das Objekt zuerst konstruiert und dann erst den problematischen Funktionsaufruf durchführt. Aber so ganz elegant ist das auch nicht.
    Lässt es sich machen, dass das Objekt im Falle einer Exception im Konstruktor nicht wieder abgebaut wird?



  • Eine Exception zu werfen, wenn der Konstruktor Mist baut, ist prinzipiell kein Problem, eigentlich die einzige Möglichkeit fehlerhafte Objekte gleich im Ansatz zu verhindern.
    Was hier fehlt ist Code! Ich hab keine Ahnung was bei dir genau abgeht... Skizzier dein Problem mit kompilierbraem Code, der das Problem aufweist!
    Oder zu mindest die involvierten Funktionen/Konstruktoren/Klassenhierarchien.



  • Athar schrieb:

    Moin,

    folgendes Problem: in einem Konstruktor wird zum Schluss eine Funktion aufgerufen, die unter bestimmten Umständen veranlasst, dass die gerade erstellte Instanz gleich wieder gelöscht wird.

    Muss das so? Kommt mir bäh-bäh vor. Ein Objekt gilt erst dann als erzeugt, wenn der Konstruktor durchgelaufen ist. Du löschst praktisch ein unvollständiges Objekt.
    Mal davon abgesehen, dass ich solche Objekte nicht auf dem Stack anlegen kann, erwarte ich, dass ein new MyClass mir entweder einen Zeiger auf ein gültiges Objekt liefert, oder eine Exception wirft. Einen Zeiger auf ein kaputtes Objekt will ich nicht haben.



  • Als Minimalbeispiel mag folgendes dienen:

    #include <stdexcept>
    
    class SelfDeletionException : public std::exception
    {
      public:
        virtual const char* what() const throw()
        {
          return "Object got deleted during a member function call.";
        }
    };
    
    class MeineKlasse
    {
      public:
        MeineKlasse(int x) : test("test")
        {
          //...
          problematischeFunktion(x);
        }
        void problematischeFunktion(int x)
        {
          delete this;
          throw SelfDeletionException();
        }
    
      private:
        std::string test;
    };
    
    int main ()
    {
      try
      {
        new MeineKlasse(1);
      }
      catch(SelfDeletionException&) {}
    }
    

    Bashar schrieb:

    Muss das so? Kommt mir bäh-bäh vor. Ein Objekt gilt erst dann als erzeugt, wenn der Konstruktor durchgelaufen ist. Du löschst praktisch ein unvollständiges Objekt.

    Die problematische Funktion setzt die Startposition des Objekts. Es macht durchaus Sinn, die Startposition im Konstruktor zu übergeben, da jede Instanz auf jeden Fall eine gültige Position haben muss.
    Allerdings wird bei jeder Positionsänderung eines Objekts ein Signal ausgelöst, das interessierte Listener über Objektbewegungen informiert. Und einer der Listener kann das Objekt u.U. indirekt löschen.
    Offiziell mag die Konstruktion noch nicht fertig sein, aber inoffiziell ist das Objekt am Ende des Konstruktors eigentlich vollständig "handlungsfähig" und das sehen andere Objekte auch so.

    Bashar schrieb:

    Mal davon abgesehen, dass ich solche Objekte nicht auf dem Stack anlegen kann, erwarte ich, dass ein new MyClass mir entweder einen Zeiger auf ein gültiges Objekt liefert, oder eine Exception wirft. Einen Zeiger auf ein kaputtes Objekt will ich nicht haben.

    Dass die Objekte nicht auf dem Stack angelegt werden können ist okay, da es durch die Natur der Objekte auch keinen Sinn machen würde.
    Und genauso soll es auch sein: entweder die Instanziierung klappt oder es wird eine Exception geworfen. Nur macht mir die Mechanik der Sprache einen Strich durch die Rechnung, da sie das Objekt noch ein weiteres Mal löschen möchte.



  • Ja, das ist einfach so, das delet this ist eigentlich überflüssig.
    Tritt im Konstruktor eine Exception auf wird das koplette Objekt wieder "zurückgebaut". Es ist danach zerstört.



  • l'abra d'or schrieb:

    Ja, das ist einfach so, das delet this ist eigentlich überflüssig.
    Tritt im Konstruktor eine Exception auf wird das koplette Objekt wieder "zurückgebaut". Es ist danach zerstört.

    Ja, in dem Minimalbeispiel ist es natürlich überflüssig, aber im konkreten Fall sieht das anders aus. Das Objekt, das die Instanz löscht, kann von außen nicht erkennen, ob sie schon fertig konstruiert ist oder nicht. Und das soll es auch gar nicht, da es kein korrektes Verhalten wäre, wenn das Objekt eine Sonderbehandlung erfahren würde, nur weil es "gerade eben erst" konstruiert wurde (oder offiziell: noch konstruiert wird).



  • Athar schrieb:

    l'abra d'or schrieb:

    Ja, das ist einfach so, das delet this ist eigentlich überflüssig.
    Tritt im Konstruktor eine Exception auf wird das koplette Objekt wieder "zurückgebaut". Es ist danach zerstört.

    Ja, in dem Minimalbeispiel ist es natürlich überflüssig, aber im konkreten Fall sieht das anders aus. Das Objekt, dass die Instanz löscht, kann von außen nicht erkennen, ob sie schon fertig konstruiert ist oder nicht. Und das soll es auch gar nicht, da es kein korrektes Verhalten wäre, wenn das Objekt eine Sonderbehandlung erfahren würde.

    Ich würde sagen, Dein Beispiel reicht so nicht aus, um Dein Problem zu verstehen.
    Eine Exception bei der Objektkonstruktion sorgt dafür, dass es das Objekt gar nicht erst gibt.



  • wenn du im konstruktor eine exception wirfst brauchst du kein delete this. Denn 1) wenn du das Objekt nicht per new erzeugt hast würde der operator delete für Speicher auf dem Stack aufgerufen -> autsch.
    2) wenn das Objekt per new erzeugt wird und im Konstruktor eine exceptin fliegt, wird der Speicher automatisch freigegeben wenn die exceptino aus dem Ctor fliegt. Wenn vorher bereits delete aufgerufen wird -> autsch.
    Also lass das delete this einfach weg, und alles wird gut.



  • Ist es ungefähr so?

    void ObjectManager::insert( Object* o ) {
        if( this->outOfBounds(o) ) {
            delete o;
        }
    }
    
    Object::Object() {
        ObjectManager::inert(this); // mögliches delete
    }
    

    Ansonsten musst du dir was eigenes zurechtzimmern, was ohne Komplikationen deine Objekte zerstört. Z.B. über ein Flag:

    void ObjectManager::insert( Object* o ) {
        if( this->outOfBounds(o) ) {
            o->deleteLater();
        }
    }
    
    Object::Object() {
        ObjectManager::inert(this);
        if(m_deleteLater)
            throw SelfDeletionException();
    }
    

    Aber schön ist das immer noch nicht...



  • Da ein kompilierbares Beispiel etwas lange dauern würde, versuche ich es mal zu beschreiben:

    ich habe eine Klasse NPC. Wie jedes Objekt hat er eine Position in der Welt, die im Konstruktor übergeben wird. Wenn alles soweit initialisiert ist, wird move() mit der Startposition als Parameter aufgerufen. move benachrichtigt die Map, auf die das Objekt bewegt wurde, dass eine Positionsänderung stattgefunden hat. Wenn das Objekt nach dieser Benachrichtigung nicht mehr existiert, erkennt move() das und wirft eine Exception. Die Map löst als Antwort auf die Benachrichtigung ein Signal aus, das alle Listener über die Positionsänderung des Objekts informiert. Unter anderem ist da eine Falle dabei, die auf einem Feld liegt und dem ersten NPC, der ihr Feld betritt, Schaden zufügt.

    Das Problem tritt auf, wenn der NPC direkt auf dem Feld der Falle erzeugt wird und weniger Trefferpunkte hat als die Falle Schaden zufügt, dann stirbt er nämlich sofort. Das ist zwar durchaus so gewollt, führt aber zum Problem der Doppellöschung.

    Einerseits sehe ich das Initialisieren der Position im Konstruktor als sinnvoll an und andererseits funktioniert es wegen eigentlich nur einer Kleinigkeit nicht.

    Edit:
    @l'abra d'or
    Ein "deleteLater" wäre in der Tat auch nicht so elegant. Vor allem da es wichtig ist, dass der Konstruktor sofort aufgerufen wird, sonst könnte das evtl. aus dem Ruder laufen, wenn ein an sich schon totes Objekt sich nicht sofort sauber aus der Welt entfernt.

    Mit einer create-Funktion geht es ja, da es erlaubt ist, ein Objekt kurzzeitig auf eine "Nullposition" zu setzen, wenn es sofort danach auf eine gültige Map gesetzt wird. Bei einer Nullposition ist auch garantiert, dass das Signal nicht ausgelöst wird. Aus der create-Funktion tritt die Exception auch problemlos aus. Es stört mich nur, dass dieser Umweg überhaupt nötig ist und hoffte, es würde auch ohne gehen.



  • Wenn eine Exception bei der Konstruktion eines mit new angeforderten Objektes fliegt, wird der entsprechende operator delete aufgerufen. Du darfst also nicht selbst freigeben. Im Übrigen hat das pumuckl schon erwähnt.

    Ich halte allerdings generell nicht besonders viel davon, wenn sich Objekte selbst löschen. Meiner Ansicht nach sollte das derjenige tun, der sie anfordert. Eine Möglichkeit wäre, ein Signal an die verwaltende Klasse zu senden, welche das Objekt gleich aus dem Container entfernt (bei einer Exception im Konstruktor werden die Objekte jedoch gar nicht erst eingefügt). Wenn das im Konstruktor geschieht, ist das allerdings etwas heikel, da ein Objekt sozusagen während seiner Konstruktion zerstört wird.

    Alternativ kannst du auch mit Status-Flags arbeiten und Spiellogik und Löschung zeitlich trennen. Das wäre sicher einfach zu implementieren. Ausserdem könntest du dir dadurch Performancevorteile verschaffen, weil immer gleich mehrere Objekte aufs Mal gelöscht werden können (je nach Datenstruktur ist das nämlich schneller).


Anmelden zum Antworten