std::map Verständnis Frage

  • Hallo,


    mir fehlt das Verständnis für folgenden Code:



    Die Ausgabe lautet:


    ~test
    ~test
    exit
    ~test



    Mir ist vollkommen unklar wieso beim Einfügen der Instanz von 'test' der Konstruktor zweimal aufgerufen wird. Die Map ist doch eigentlich nur ein Container, der den Inhalt eigentlich nicht anfassen sollte.


    Man möge mich erhellen :schiel


    Gruß
    Sebastian

  • Diese Zeile weist keine Referenz zu, sondern kopiert das Objekt, deshalb kommen mehrere Instanzen zustande.
    m_map[1] = *(new test());


    Wenn Du statt dem Objekt, nur einen Pointer auf das Objekt verwendet,
    dann kann dieser Kopiert werden ohne das es stört, man muss sich aber selber
    um die Lebenszeit und das Löschen per delete kümmern.



    Andreas


  • Dass es 3 Kopien sind, ist leicht erklärt. Die erste erstellst du mit dem new-Operator. Die zweite ist ein sogenanntes temporary object: da die rechte Seite deiner Zuweisung ein Ausdruck ist (dereferenzierung von irgendwas) wird hier ein temporäres Objekt mit dem Ergebnis des Ausdrucks erstellt, dessen Lebensdauer sich auf die eine Anweisung beschränkt. Für die dritte Kopie ist dann std::map<> verantwortlich, denn das ist, wie du schon sagst, ein Container. Wenn es Objekte einer Klasse speichern soll tut es auch genau das (und speichert nicht etwa Pointer oder Referenzen), also muss es das Objekt kopieren, das gespeichert werden soll.


    Was mich bei deiner Ausgabe wundert ist, dass der Destruktor der ersten Kopie überhaupt aufgerufen wird. Da du den Rückgabewert von new nicht speicherst sondern direkt weiterverarbeitest (in dem Fall mit *), ist der Pointer zu deiner ersten Kopie eigentlich verloren, das sollte normalerweise ein Speicherleck sein.


    Was du wahrscheinlich tun willst ist wie von meinem Vorposter schon vorgeschlagen, Pointer im Container zu speichern. Diese bergen natürlich Gefahren, sobald der Code etwas komplexer wird und nicht mehr auf einen Blick klar ist, wo und wann das Objekt zerstört werden muss -- eventuell willst du dir "Smart Pointers" anschauen.

    Asrock A75 Pro4-M
    Debian wheezy (testing, stock) (aktuell 2012-08-24: Linux 3.2, VDR 1.7.28)
    vdr-sxfe (xineliboutput)
    Pioneer VSX-520-K

  • Zitat

    Originally posted by zuse
    [...]
    Mir ist vollkommen unklar wieso beim Einfügen der Instanz von 'test' der Konstruktor zweimal aufgerufen wird. Die Map ist doch eigentlich nur ein Container, der den Inhalt eigentlich nicht anfassen sollte.
    [...]


    <stdlib.h> ersetzt du bitte durch <iostream>
    und
    printf("whatever"); ersetzt du bitte durch std::cout << "whatever" << std::endl;


    ausgehend von std::map<int, test>:


    m_map[1] = *(new test()); <--- falsch + gefaehrlich


    m_map[1] = test(); <--- richtig


    Das von dir bebachtete Verhalten sind fundamentale Eigenschaften von C++.
    Da du keinen copy ctor definiert hast, erstellt der C++ Compiler für dich einen default copy ctor. Von deinem erstellten Objekt werden bei verschiedenen Assignment-Operationen kurzfristige, für dich nicht ersichtliche Kopien erstellt. Dies sind aber ganz normale Instanzen deiner Klasse, dessen dtor nach der Verwendung ausgeführt wird. Genau dieses Verhalten hast du beobachtet!


    Wichtig ist hier auch:
    m_map[1] erstellt zuerst! ein neues Objekt vom Typ 'test', fügt es in die map ein und gibt dann eine Referenz auf das neue Objekt zurück. Im nächsten Schritt wird der Assignment-Operator operator=(const test& rhs) dieses Objekts aufgerufen mit test() als Argument. Das Argument wird ebenfalls nach der Assignment-Operation zerstört.



    Btw. wenn du mit new arbeitest, schau dir vorher std::auto_ptr<> bzw. boost::shared_ptr<> und boost::weak_ptr<> an!



    Zitat

    Originally posted by zirias
    Dass es 3 Kopien sind, ist leicht erklärt. Die erste erstellst du mit dem new-Operator. Die zweite ist ein sogenanntes temporary object: da die rechte Seite deiner Zuweisung ein Ausdruck ist (dereferenzierung von irgendwas) wird hier ein temporäres Objekt mit dem Ergebnis des Ausdrucks erstellt, dessen Lebensdauer sich auf die eine Anweisung beschränkt. Für die dritte Kopie ist dann std::map<> verantwortlich, denn das ist, wie du schon sagst, ein Container. Wenn es Objekte einer Klasse speichern soll tut es auch genau das (und speichert nicht etwa Pointer oder Referenzen), also muss es das Objekt kopieren, das gespeichert werden soll.


    Das stimmt so leider nicht.



    Zitat

    Originally posted by zirias
    Was mich bei deiner Ausgabe wundert ist, dass der Destruktor der ersten Kopie überhaupt aufgerufen wird. Da du den Rückgabewert von new nicht speicherst sondern direkt weiterverarbeitest (in dem Fall mit *), ist der Pointer zu deiner ersten Kopie eigentlich verloren, das sollte normalerweise ein Speicherleck sein.


    Der Destruktor des durch new() instantiierten Objekts wird in diesem Beispiel nie ausgefuehrt.

  • Zitat

    Original von crib
    m_map[1] erstellt zuerst! ein neues Objekt vom Typ 'test', fügt es in die map ein und gibt dann eine Referenz auf das neue Objekt zurück. Im nächsten Schritt wird der Assignment-Operator operator=(const test& rhs) dieses Objekts aufgerufen mit test() als Argument. Das Argument wird ebenfalls nach der Assignment-Operation zerstört.


    So ganz stimmt das immer noch nicht, die rechte Seite ist nicht test() sondern *(new test()) und damit ein temporary...


    Zitat

    Das stimmt so leider nicht.


    Naja, hier war ich wohl in Gedanken bei std::vector<>::push_back(). Aber bis auf die Kopie beim Einfügen stimmt's schon...


    Zitat

    Der Destruktor des durch new() instantiierten Objekts wird in diesem Beispiel nie ausgefuehrt.


    Das sehe ich ja genauso. Aber woher kommt der dritte Destruktor-Aufruf? Egal wie ich es drehe, ich sehe bei dem Code abgesehen vom mit new konstruierten Objekt nur zwei weitere Objekte.


    Und etwas OT: Ob IOStreams oder Stdlib ist durchaus Geschmackssache. IOStreams ist eben die neuere und objektorientierte Lösung, aber deshalb nicht unbedingt in JEDEM Fall die beste Wahl. Wenn man sich die Mechanismen dahinter mal anschaut, KANN das auch mal mit Kanonen auf Spatzen sein ;)

    Asrock A75 Pro4-M
    Debian wheezy (testing, stock) (aktuell 2012-08-24: Linux 3.2, VDR 1.7.28)
    vdr-sxfe (xineliboutput)
    Pioneer VSX-520-K

    Einmal editiert, zuletzt von zirias ()

  • Zitat

    So ganz stimmt das immer noch nicht, die rechte Seite ist nicht test() sondern *(new test()) und damit ein temporary...


    Der Ausdruck "*(new test())" ist in sich bereits ein memleak und damit ein schwerer Fehler. Was du mit new allokierst, musst du auch wieder frei geben.
    Aber selbst wenn du es wieder frei geben würdest, es wäre trotzdem schlechter Code. Das temporäre Objekt gehört auf den Stack, wie in meinem Beispiel!


    Zitat

    Naja, hier war ich wohl in Gedanken bei std::vector<>::push_back(). Aber bis auf die Kopie beim Einfügen stimmt's schon...


    Nein tut es nicht.


    Zitat

    Das sehe ich ja genauso. Aber woher kommt der dritte Destruktor-Aufruf? Egal wie ich es drehe, ich sehe bei dem Code abgesehen vom mit new konstruierten Objekt nur zwei weitere Objekte.


    Insgesamt werden VIER Objekte erstellt und damit VIER Konstruktoren und VIER Destruktoren ausgeführt!


    Im Detail:


    Wir gehen aus von:
    std::map<int, test> mymap;
    mymap[1] = test();


    mymap[1] führt zu:


    neues objekt wird erstellt (1)
    kopie von (1) wird erstellt (2)
    kopie von (2) wird erstellt (3)
    (1) wird zerstoert
    (2) wird zerstoert
    (3) befindet sich jetzt in der map


    bis hierher ist es das interne Verhalten von std::map<>


    jetzt kommt unsere Zuweisung: ' = test()'


    temporaeres objekt wird erstellt (4)
    der assignment-operator von (3) wird mit (4) als parameter aufgerufen
    (4) wird zerstoert


    Am Ende des Programms wird mymap zerstört und damit alle Destruktoren der enthaltenen Objekte aufgerufen, dies führt zu:


    (3) wird zerstört


    ----------------------------------------


    Dieses elementare Verhalten zu verstehen ist ungemein wichtig, wenn man C++ programmieren will. Insbesondere den Code, den der C++ Compiler selber erzeugt, ohne dass man davon weiss (copy ctor, assigment operator, ...).

  • Noch was:


    Schreibe die Klasse 'test' mit einem default ctor, einem copy ctor und einem assignment operator und schau, was folgender Code macht:


    mymap[1];


    Dann geht dir vielleicht ein Licht auf. ;)

  • Der Grund warum in der Map so viel kopiert wird, ist, dass die Map intern ein Container von pair<key_type, mapped_type> ist. Dröseln wir den Code mal auf:


    Code
    m_map[1] = test(); // entspricht:
    
    
    test& t = m_map[1]; // m_map.operator[](1)
    t = test(); // t.operator=(test());


    Beim ersten Zugriff auf m_map[1] wird ein insert(end(), pair<int, test>(1, test())) aufgerufen. Hierbei wird ein temporäres test als Argument für den pair-Konstruktor erzeugt ("test()") und ein test als Member second innerhalb des pair selbst (welches aus dem Argument Copy-Konstruiert wird). Das Objekt vom Typ pair ist selbst natürlich auch temporär. Bis hierher haben wir schon zwei Objekte.


    Innerhalb dieser Methode insert, welche eine Referenz auf ein pair als Parameter erhalten hat, wird nun ein Knoten im Baum erzeugt, an dessen Stelle das pair wiederum Copy-Konstruiert wird. Dieses dritte Objekt lebt nun erst tatsächlich im Baum.


    Nach dem Abarbeiten des Statements m_map[1] werden die beiden temporären Objekte wieder abgebaut, wodurch zwei Destruktoren durchlaufen werden. Das Ergebnis von m_map[1] ist eine Referenz auf test.


    Diesem Objekt, dessen Referenz wir in t speichern, weisen wir ein weiteres temporäres Objekt zu (4. Konstruktor). Dabei wird der Assignment-Operator aufgerufen und der Inhalt des temporären Objekts in das Objekt in der Map kopiert. Danach wird das temporäre Objekt wieder zerstört (3. Destruktor).


    Am Ende des Programms wird natürlich die Map wieder abgebaut, wobei jedes darin enthaltene Objekt zerstört wird (4. Destruktor).

  • Zitat

    Original von crib


    Der Ausdruck "*(new test())" ist in sich bereits ein memleak und damit ein schwerer Fehler. Was du mit new allokierst, musst du auch wieder frei geben.
    Aber selbst wenn du es wieder frei geben würdest, es wäre trotzdem schlechter Code. Das temporäre Objekt gehört auf den Stack, wie in meinem Beispiel!


    Und wo hab ich geschrieben, dass das richtig sei? Es geht um nichts anderes als alle Instanzen des Objekts zu finden. Ist ja toll, dass du wiederholt darauf hinweist, wie fürchterlich falsch das ist, aber das hat der OP schon zweimal gesagt bekommen, bevor du überhaupt in den Thread eingestiegen bist.


    Zitat


    Nein tut es nicht.


    Doch, tut es. Eine Instanz durch new, eine durch temporary und eine in der Map haben wir, genau wie ich geschrieben habe. Die vierte hat mir gefehlt, das hat jetzt aber LordJaxom erfreulicherweise aufgeklärt (durch std::pair). Es reicht als Erklärung eben nicht ganz aus zu sagen, dass gleich zweimal kopiert wird, man muss dann schon auch erfahren, warum ;)

    Asrock A75 Pro4-M
    Debian wheezy (testing, stock) (aktuell 2012-08-24: Linux 3.2, VDR 1.7.28)
    vdr-sxfe (xineliboutput)
    Pioneer VSX-520-K

Jetzt mitmachen!

Sie haben noch kein Benutzerkonto auf unserer Seite? Registrieren Sie sich kostenlos und nehmen Sie an unserer Community teil!