std::vector / Element-Destruktor



  • Hallo,

    ich bin in meinem Programm gerade auf ein kleines Problem mit einem meiner Vektoren gestoßen. Kurz gesagt speichere ich darin Objekte (by value, keine Pointer), wobei natürlich der vector des öfteren vergrößert wird.

    Ich hab das Ganze zum Ausprobieren in eine kleine Demo zusammengefasst:

    class Test
    {
    public:
    	Test(int x) {
    		this->x = x;
    	}
    
    	Test(const Test& test) {
    		this->x = test.x;
    	}
    
    	virtual ~Test() {
    		printf("deleting %d\n", x);
    	}
    
    private:
    	int x;
    };
    
    int main(void) {
    	std::vector<Test> tests;
    	tests.push_back(1);
    	tests.push_back(2);
    	tests.push_back(3);
    	tests.push_back(4);
    	tests.push_back(5);
    
    	for(TestList::iterator it=tests.begin(); it!=tests.end();) {
    		it = tests.erase(it);
    	}
    
    	return 0;
    }
    

    Laut Ausgabe wird in der Schleife am Ende 5mal der Destruktor des letzten Elements aufgerufen (Es wird 5mal "deleting 5" ausgegeben), weil im Vektor zuerst alle Daten-Elemente um einen Platz nach vorne verschoben werden, und dann jedesmal das hinterste Objekt zerstört wird.
    Leere ich den Vektor stattdessen mit tests.clear(), wird jedes Element genau einmal gelöscht.
    Ist das aus irgendeinem Grund so beabsichtigt, oder ist das ein Fehler? Ich bin bisher davon ausgegangen, daß immer der Destruktor des Elements aufgerufen wird, das ich auch gerade löschen will.

    Beim Hinzufügen in den Vektor werden natürlich auch fröhlich Objekte erzeugt und gelöscht, wenn der Vektor neu allokiert wird, aber zumindest hier bei dem Test zumindest genau die richtigen. Ob das im richtigen Programm auch passt weiß ich leider noch nicht sicher.

    Bei meinen echten Objekten bin ich jedenfalls darauf angewiesen, daß jeder Destruktor nur einmal aufgerufen wird. Gibt es eine Möglichkeit, das für den vector irgendwie sicherzustellen?

    Gestestet übrigens mit gcc 4.7.3 unter Linux und MinGW 4.7.2 unter Windows.



  • ...



  • Ja, das stimmt - es ist immer das gerade letzte Element im Vector.

    Ich habe im Debugger den entsprechenden Code-Teil angesehen, grob gesagt steht da
    - schiebe alle verbleibenden Elemente um 1 nach vorn (dabei wird das per erläse zu löschende Element überschrieben)
    - dekrementiere den "end" Zeiger (zeigt nun auf das ehemals letze Element: 5, also nach der Verkleinerung der liste auf eine stelle hinter der Liste)
    - Rufe den Destruktor des Elements beim end-Zeiger auf

    (Gerade nicht am PC, kann später gern noch den entsprechenden Code aus der Vector-Klasse Posten)

    Eigentlich sollte ja eher zuerst der Destruktor bei dem Iterator aufgerufen werden, den ich löschen will, dann erst die anderen Elemente verschoben werden.



  • mycpp schrieb:

    Ist das aus irgendeinem Grund so beabsichtigt, oder ist das ein Fehler? Ich bin bisher davon ausgegangen, daß immer der Destruktor des Elements aufgerufen wird, das ich auch gerade löschen will.

    Das ist richtig so. Schließlich würde der Aufruf des Destruktors eines nicht am Ende befindlichen Elementes eine Lücke erzeugen. Das wäre in Hinblick auf Exceptionsicherheit fatal.

    In einer exceptiuonsicheren Umgebung sollte die Zerstörung von Objekten immer der letzten Schritt einer modifizierenden Operation sein, niemals der Erste.



  • Danke, das ist schonmal interessant zu wissen.

    Nun ist es in meinem richtigen Programm so, daß ich durch diesen Umstand Speicherfehler bekomme. Die Elemente 1 bis 4 besitzen Daten, die nie gelöscht werden, im Gegensatz dazu werden die Daten von Element 5 gleich mehrfach gelöscht (bzw. tritt schon beim zweiten Versuch eine Zugriffs-Verletzung auf).

    Wie sollte man am besten damit umgehen?
    std::list speichert die Daten ja anders, so daß der Effekt hier nicht auftritt, aber gibts dafür eine Garantie, oder existieren da auch wieder Sonderfälle?



  • mycpp schrieb:

    Nun ist es in meinem richtigen Programm so, daß ich durch diesen Umstand Speicherfehler bekomme. Die Elemente 1 bis 4 besitzen Daten, die nie gelöscht werden, im Gegensatz dazu werden die Daten von Element 5 gleich mehrfach gelöscht (bzw. tritt schon beim zweiten Versuch eine Zugriffs-Verletzung auf).

    Hast Du den Zuweisungsoperator implementiert?



  • Ja - in dem Test oben hab ich ihn zwar vergessen, im richtigen Projekt ist er aber drin.

    Dürfte in dem Fall aber aktuell auch keine Rolle spielen. Wenn ich Element 1 lösche wurden die Elemente 2-5 alle richtig verschoben.

    Aus "1 - 2 - 3 - 4 - 5" wird dann "2 - 3 - 4 - 5 - 5", wobei dann der Destruktor für das letzte Element, der zweiten fünf, aufgerufen wird.

    Hier einmal die Implementation von erase aus dem gcc 4.7.3 in Ubuntu:

    template<typename _Tp, typename _Alloc>
        typename vector<_Tp, _Alloc>::iterator
        vector<_Tp, _Alloc>::
        erase(iterator __position)
        {
          if (__position + 1 != end())
    	_GLIBCXX_MOVE3(__position + 1, end(), __position);
          --this->_M_impl._M_finish;
          _Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);
          return __position;
        }
    


  • mycpp schrieb:

    Ja - in dem Test oben hab ich ihn zwar vergessen, im richtigen Projekt ist er aber drin.

    Aber offenbar fehlerhaft.



  • Weil?

    Es wird ja trotzdem noch das falsche Objekt gelöscht.
    Bei tests.erase(tests.begin()) soll ja der Destruktor von 1 aufgerufen werden, nicht der von 5. Es wird ja nirgendwo eine Zuweisung von 1 auf 5 gemacht.


  • Mod

    mycpp schrieb:

    Weil?

    Es wird ja trotzdem noch das falsche Objekt gelöscht.
    Bei tests.erase(tests.begin()) soll ja der Destruktor von 1 aufgerufen werden, nicht der von 5. Es wird ja nirgendwo eine Zuweisung von 1 auf 5 gemacht.

    Das heißt nicht umsonst "Regel der großen Drei". Dein Zuweisungsoperator soll gefälligst die alten Ressourcen löschen. Dann ist alles korrekt.

    P.S.: Falls du es nicht kennen solltest: Dieses Zuweisung und Löschen kann man einfacher Implementieren, als man als Anfänger selber drauf kommt:
    http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Copy-and-swap
    Vorteile:
    1. Einfachere, kürzere Implementierung als die naive Zuweisung
    2. Exceptionsicher



  • Ansich sollte der Zuweisungs-Operator das bereits tun, da ich diese Klasse schon öfter verwendet habe.
    Ich werd mir die Stelle aber noch mal genauer ansehen, falls sich da doch noch ein Fehler versteckt haben sollte.


Anmelden zum Antworten