Kopier-Konstruktor


  • Mod

    So als Zusatzinfo, da es mich beim ersten Mal sehr überrascht hat und sicherlich auch einige andere überraschen wird: Selbst wenn keine richtige Kopie erstellt wird, darf sie wegoptimiert werden:

    struct Foo
    {
      int blah;
      Foo(int i) : blah(i) { cout << "Konstruktor " << blah << '\n'; }
      Foo(const Foo& other): blah(1) {cout << "Falscher Kopierkonstruktor " << blah << '\n'; }
    };
    
    int main()
    {
      Foo t1(5);
      Foo t2(t1);
      Foo t3(Foo(7));
      cout << t3.blah << '\n';
    }
    

    Hier darf (und wird) tatsächlich 7 anstatt 1 ausgegeben werden.



  • Könnte es auch nicht passieren, dass der Compiler zunächst ein leeres Objekt erzeugt und anschließend über den Zuweisungsoperator das Objekt initialisiert? Damit meine ich: Darf der Compiler einen Ausdruck wie

    Test t3(Test(2));
    

    ersetzen durch etwas Ähnlichem wie:

    Test t3(0);
    t3.operator=(Test(2));
    


  • Nein. Selbst das hier

    Foo f = Foo(4);
    // oder
    Foo f2 = 4;
    

    Ist kein Zuweisungsoperator, sondern der Kopierkonstruktor.



  • Nein darf er nicht.



  • Aber in 2 Schritten doch nicht, oder?

    Foo f = Foo(4);
    f = Foo(5);
    

    Erste Zeile: Initialisierung und damit Kopierkonstruktor oder Konstruktor mit Eliminierung des temporären Objekts. Zweite Zeile: Zuweisung. Die Frage wäre, ob der Compiler einen Initialisierungsausdruck umwandeln darf in eine Art Zweizeiler mit Zuweisung.



  • Nein darf er nicht. 😉



  • Danke 🙂


  • Mod

    Wie soll er denn zuerst ein "leeres" Objekt erstellen? Definier mal "leer" in C++-Sprech, dann siehst du, warum das nicht gehen kann (außer in Ausnahmefällen).



  • Mit "leerem Objekt" meine ich: Der Compiler macht nur Speicher für das Objekt frei ohne irgendwelche Komponenten zu initialisieren. Danach Zuweisung.


  • Mod

    Ulrike_1992 schrieb:

    Mit "leerem Objekt" meine ich: Der Compiler macht nur Speicher für das Objekt frei ohne irgendwelche Komponenten zu initialisieren. Danach Zuweisung.

    Und genau das ist bei komplexen Datentypen nicht möglich. Denk mal nach, wie zum Beispiel der Zuweisungsoperator eines vectors aussehen muss. Der muss irgendwo den Speicher des überschriebenen vectors freigeben. Wenn der interne Datenzeiger aber niemals initialisiert wurde und stattdessen irgendeinen Müllwert hat, dann stürzt das zur Laufzeit ab.



  • Jain, das wäre ja genau der Zeitpunkt im operator new, wenn gearde Speicher allokiert wurde, aber der Konstruktor noch nicht aufgerufen wurde.

    Aber letztlich hat SeppJ recht, hör auf ihn. Denn wenn du irgendwelche Spielereien im Speicher machen willst, kommst du schneller in Teufels Küche als der Teufel persönlich.



  • Die einzige sinnvolle Definition für "leeres" Objekt ist ein default-konstruiertes Objekt (also mit dem Konstruktor ohne Argumente). Es hat aber nicht jede Klasse einen Default-Konstruktor, so dass das nicht allgemein funktionieren kann.



  • Bashar schrieb:

    Die einzige sinnvolle Definition für "leeres" Objekt ist ein default-konstruiertes Objekt (also mit dem Konstruktor ohne Argumente). Es hat aber nicht jede Klasse einen Default-Konstruktor, so dass das nicht allgemein funktionieren kann.

    Meine Frage war weniger, ob es dem Compiler immer erlaubt ist, den Kopierkonstruktor durch eine Art Zuweisung zu ersetzen. Meine Frage ist mehr, ob der Compiler unter bestimmten Umständen, z.B. wenn ein Default-Konstruktor vorhanden ist, eine Initialisierung durch einen Kopier-Konstruktor dadurch ersetzen darf, dass er erst ein "default"-Objekt erzeugt, und anschließend mit einer Zuweisung dessen Komponenten initialisiert. Eigentlich sollten doch Kopier-Konstruktor und Zuweisungs-Operator eng miteinander verwandt sein. Ein Ausdruck wie

    class Foo
    ...
    Foo f;
    Foo g(f);
    

    sollte doch das Gleiche bewirken wie

    Foo f;
    Foo g;
    g = f;
    

    Folgende Klasse Foo würde diese Erwartung erfüllen:

    struct Foo
    {
        int x;
        Foo() : x(0) {}
        Foo(const Foo& f) {
            this->operator=(f);
        }
        const Foo& operator=(const Foo& f) {
            this->x = f.x;
            return *this;
        }
    };
    

    Nun können anscheinend Kopier-Konstruktor und Zuweisungs-Operator unsinnigerweise so definiert sein, dass sie etwas vollkommen Anderes bewirken. Aber der Compiler könnte möglicherweise davon ausgehen, dass beide Komponenten dasselbe bewirken und ersetzt möglicherweise den Rumpf des Kopier-Konstruktors durch this->operator=(irgendwas) Ich frage mich einfach: Wie weit darf der Compiler das erwartete Verhalten zugunsten einer Optimierung verändern.



  • Ich frage mich einfach: Wie weit darf der Compiler das erwartete Verhalten zugunsten einer Optimierung verändern.

    Grundsätzlich: Überhaupt nicht. Die Auslassung von Kopien ist die einzige Ausnahme von dieser Regel.



  • Ulrike_1992 schrieb:

    class Foo
    ...
    Foo f;
    Foo g(f);
    

    sollte doch das Gleiche bewirken wie

    Foo f;
    Foo g;
    g = f;
    

    Eher genau umgekehrt:

    Foo f;
    Foo g = f;
    // oder
    Foo g;
    g = f;
    

    Wird zu:

    Foo f;
    Foo g(f);
    


  • Skym0sh0 schrieb:

    Foo f;
    Foo g = f;
    // oder
    Foo g;
    g = f;
    

    Wird zu:

    Foo f;
    Foo g(f);
    

    Dass ein Ausdruck wie Foo g = f eine andere Schreibweise ist für Foo g(f) ist mir schon bewusst. Worauf ich hinaus wollte ist, dass eine Initialisierung der Form Foo g(f) ein Objekt erzeugen sollte, das inhaltlich gleich ist mit einem Objekt, das zunächst mit dem Default-Konstruktor Foo g erzeugt wurde und das man anschließend in einer Zuweisung g = f ändert.

    Aus dem Grund sollte es dem Compiler eigentlich möglich sein, einen Kopierkonstruktor zu erzeugen, wenn er den Zuweisungsoperator kennt. Er würde dann einfach den Funktionsrumpf des Kopierkonstruktors ersetzen durch this->operator=(foo_objekt_oder_wie_immer_die_Klasse_lautet) .

    Ein Programmierer könnte aber den Kopierkonstruktor und den Zuweisungsoperator so definieren, dass überraschenderweise ein Dreizeiler wie

    Foo f;
    Foo g;
    g = f;
    

    ein Objekt g erzeugt, das sich inhaltlich unterscheidet von dem Objekt g , das erzeugt wird durch folgenden Zweizeiler:

    Foo f;
    Foo g(f);
    

    Wie ich ja jetzt gelernt habe, geht der Compiler (Copy Elision) davon aus, dass das Objekt f das durch einen Kopier-Konstruktor (mit temporärem Objekt) erzeugt wird:

    Foo f(Foo(p1, p2, p3));
    

    inhaltlich identisch ist mit dem Objekt f das durch den Aufruf eines Konstruktors mit Parameterliste erzeugt wird:

    Foo f(p1, p2, p3);
    

    Auch wenn der Kopierkonstruktor (unsinnigerweise) so definiert sein könnte, dass er eigentlich ein ganz anderes Objekt erzeugen sollte als über einen Konstruktor mit Parameterliste.


  • Mod

    Ulrike_1992 schrieb:

    Aus dem Grund sollte es dem Compiler eigentlich möglich sein, einen Kopierkonstruktor zu erzeugen, wenn er den Zuweisungsoperator kennt. Er würde dann einfach den Funktionsrumpf des Kopierkonstruktors ersetzen durch this->operator=(foo_objekt_oder_wie_immer_die_Klasse_lautet) .

    Nein, das geht eben nicht. Ein Gegenbeispiel wurde dir schon genannt.



  • SeppJ schrieb:

    Ein Gegenbeispiel wurde dir schon genannt.

    Ich muss leider zugeben, dass ich den Punkt noch nicht verstanden habe. Ich nehme an, du meinst das Beispiel mit dem vector . Aber erzeugen nicht folgende zwei Codefragmente den inhaltlich gleichen vector v2 ?

    Version 1:

    std::vector<int> v1;
    v1.push_back(3);
    std::vector<int> v2;
    v2 = v1;
    

    Version 2:

    std::vector<int> v1;
    v1.push_back(3);
    std::vector<int> v2(v1);
    

    Könnte der Compiler nicht Version 1 durch Version 2 ersetzen?

    • Version 1: 2 x Default-Konstruktor + 1 x Zuweisung
    • Version 2: 1 x Default-Konstruktor + 1 x Kopierkonstruktor

    Ich weiß ja nicht wie der Kopierkonstruktor für vector implementiert ist, aber Version 2 könnte schneller sein...



  • Ulrike_1992 schrieb:

    Könnte der Compiler nicht Version 1 durch Version 2 ersetzen?

    • Version 1: 2 x Default-Konstruktor + 1 x Zuweisung
    • Version 2: 1 x Default-Konstruktor + 1 x Kopierkonstruktor

    Nö, das ist doch syntaktisch und semantisch total was anderes. Hab den Thread jetzt nicht gelesen, aber kennst du den Unterschied zwischen Initialisierung und Zuweisung?


  • Mod

    😕 Version 1 durch Version 2 zu ersetzen ist doch der umgekehrte Vorgang zu dem was du gefragt hast:

    Aus dem Grund sollte es dem Compiler eigentlich möglich sein, einen Kopierkonstruktor zu erzeugen, wenn er den Zuweisungsoperator kennt. Er würde dann einfach den Funktionsrumpf des Kopierkonstruktors ersetzen durch this->operator=(foo_objekt_oder_wie_immer_die_Klasse_lautet).

    Das entspräche aus

    #include <iostream>
    #include <vector>
    #include <cstdlib>
    using namespace std;
    
    typedef vector<int> Vector;
    
    int main()
    {
      Vector original = {1,2,3,4,5};
      Vector *kopie = new Vector(original);  // Kopierkonstruktor
      for (auto a : *kopie)
        cout << a << '\n';
      delete kopie;
    }
    

    folgendes zu machen:

    #include <iostream>
    #include <vector>
    #include <cstdlib>
    using namespace std;
    
    typedef vector<int> Vector;
    
    int main()
    {
      Vector original = {1,2,3,4,5};
      Vector* kopie = static_cast<Vector*>(malloc(sizeof(Vector))); // Vollkommen uninitialisierter Vector
      *kopie = original;  // Zuweisung statt Kopierkonstruktor
      for (auto a : *kopie)
        cout << a << '\n';
      kopie->~Vector();
      free(kopie);
    }
    

    Und das ist hochgradig undefiniertes Verhalten. Ironischerweise wird das sogar zu 99% laufen (da man Nullpointer deleten kann), jedoch wird dir ein automatischer leak detector bei dem Code durchdrehen. valgrind sagt zum Beispiel:

    ==15044== Conditional jump or move depends on uninitialised value(s)
    ==15044==    at 0x400FB1: std::vector<int, std::allocator<int> >::operator=(std::vector<int, std::allocator<int> > const&) (vector.tcc:183)
    ==15044==    by 0x400C05: main (test.cc:12)
    ==15044==  Uninitialised value was created by a heap allocation
    ==15044==    at 0x4C27302: malloc (vg_replace_malloc.c:263)
    ==15044==    by 0x400BEE: main (test.cc:11)
    ==15044== 
    ==15044== Conditional jump or move depends on uninitialised value(s)
    ==15044==    at 0x401435: std::_Vector_base<int, std::allocator<int> >::_M_deallocate(int*, unsigned long) (stl_vector.h:174)
    ==15044==    by 0x40104C: std::vector<int, std::allocator<int> >::operator=(std::vector<int, std::allocator<int> > const&) (vector.tcc:189)
    ==15044==    by 0x400C05: main (test.cc:12)
    ==15044==  Uninitialised value was created by a heap allocation
    ==15044==    at 0x4C27302: malloc (vg_replace_malloc.c:263)
    ==15044==    by 0x400BEE: main (test.cc:11)
    

    P.S.: Entschuldige den Schmuddelcode mit new, malloc & Co, aber anders bekommt man eben keinen "leeren" vector hin.


Anmelden zum Antworten