(Standard- vs. Kopier-) Konstruktor



  • Hä. Du bist gerade auf Abwegen. Nochmal: Ein Kopierkonstruktor ist ein Konstruktor, der den Parameter T& hat, wobei T der Klassenname (in deinem Fall Klasse ) ist.

    Klasse(double & a):var(a){}
    

    Somit ist das also kein Kopierkonstruktor. Das hat überhaupt nichts mit einem zu tun. Ein Kopierkonstruktor erstellt ein neues Objekt anhand eines anderen Objekts der Klasse. Kopieren heißt hier, ich kopiere ein Objekt X meiner Klasse T, und nicht, ich kopiere ein Objekt X irgendeines Datentyps.



  • Klar kannst du ein Objekt auch mittels eines einfachen Konstruktors kopieren. Aber das ist doch total doof. Nicht umsonst gibt es den Kopierkonstruktor. Denn sowas

    class Klasse
    {
        public:
            Klasse(double & a):var(a){}
            double var;
    };
    int main()
    {
        Klasse a;
        Klasse b(a.var);
    }
    

    ist doch total doof. Stell dir mal vor, du hast 20 Member, dann viel Spaß. 😃



  • Ach soooo.

    Man langsam verstehe ich das ganze. Es geht nicht darum wie ich ein Objekt erstmalig initialisiere, sondern wie ich andere Objekte des gleichen Typs aus einem bereits bestehenden heraus erstelle!

    Dazu das folgende Minimalbeispiel

    #include <iostream>
    
    using namespace std;
    
    struct foo
    {
    	foo();
    	foo(const foo&);
    	foo& operator=(const foo&);
    
    	int var;
    };
    
    foo::foo():var(5)
    {
    	cout << "Constructor called!" << endl;
    }
    
    foo::foo(const foo& other):var(other.var)
    {
    	cout << "Copy construtor called!" << endl;
    }
    
    foo& foo::operator=(const foo& rhs)
    {
    	cout << "Assignment oprator called!" << endl;
    	var = rhs.var;
    	return *this;
    }
    
    int main()
    {
    
    foo f;
    
    foo f1(f);
    
    // foo f2 = f; <-- Error
    foo f2;
    
    f2 = f;
    
    return 0;
    }
    

    Mit der Ausgabe

    Constructor called!
    Copy construtor called!
    Constructor called!
    Assignment oprator called!
    

    In Zeile 34 wird das erste Objekt (die Instanz, weil von einer Klasse bzw. Struktur?) erstellt, das führt zur ersten Zeile der Ausgabe.
    Also nächstes erstelle ich in Zeile 36 die zweite Instanz von foo , durch das Kopieren von f. Das führt zur nächsten Zeile der Ausgabe.
    Schließlich möchte ich den Assignment operator verwenden, der lässt sich allerdings nur auf bereits bestehende Objekte anwenden, deshalb geht Zeile 38 vom Syntax her nicht.
    Ich muss ein neues foo zunächst in Zeile 39 erstellen und kann ihm dann den Inhalt in Zeile 41 zuordnen!

    Jetzt habe ich es doch, oder? 🙂

    Und eigentlich wäre ich jetzt fertig, nur in Zusammenhang mit STL Containern bricht das ganze wieder zusammen. Aus diesem Grund kommt die move semantik ins Spiel, ja?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Schließlich möchte ich den Assignment operator verwenden, der lässt sich allerdings nur auf bereits bestehende Objekte anwenden, deshalb geht Zeile 38 vom Syntax her nicht.

    Doch, das geht.
    Nennt sich Initialisierung, dabei wird der CopyCtor aufgerfuen.

    Und eigentlich wäre ich jetzt fertig, nur in Zusammenhang mit STL Containern bricht das ganze wieder zusammen. Aus diesem Grund kommt die move semantik ins Spiel, ja?

    Nee, move kommt aus einem anderen Grund ins Spiel.
    Nehmen wir mal an du hast eine Klasse, die irgendein HANDLE kapselt. Für dieses HANDLE fehlt aber eine Kopierfunktion (warum auch immer, und ja: kam bei mir schon mal vor).
    Die Frage ist, was machst du im CopyCtor?
    Das HANDLE kopieren? Wenn ja, wer gibt es dann frei?
    Man könnte gemeinsamen Besitz implementieren (s. shared_ptr).
    Verbietet man Kopie, hat man ein Problem mit den Container.
    Deshalb gibt es move seit C++11.
    Damit hat man das Problem nicht und auch die Container kommen damit klar.



  • Nathan schrieb:

    Klaus82 schrieb:

    Schließlich möchte ich den Assignment operator verwenden, der lässt sich allerdings nur auf bereits bestehende Objekte anwenden, deshalb geht Zeile 38 vom Syntax her nicht.

    Doch, das geht.
    Nennt sich Initialisierung, dabei wird der CopyCtor aufgerfuen.

    Ja, du hast recht. Ich war unpräzise. Ich meinte damit, dass der Assignment operator für diese Art der Initialisierung nicht benutzt wird, was per se auch nicht die Aufgabe des Assignment operator ist.
    Denn dieser dient dazu, Objekten etwas zuzweisen, die bereits initialisiert sind - so besser? 🙂

    Über den Rest muss ich erst wieder nachdenken. 🙂

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Denn dieser dient dazu, Objekten etwas zuzweisen, die bereits initialisiert sind - so besser? 🙂

    Ich würde es anders sagen:

    a = b;
    

    Kopierkonstruktor wird verwendet, wenn Objekt a nicht existiert.
    Assignment operator wird verwendet, wenn Objekt a bereits existiert.



  • out schrieb:

    a = b;
    

    Kopierkonstruktor wird verwendet, wenn Objekt a nicht existiert.
    Assignment operator wird verwendet, wenn Objekt a bereits existiert.

    Aber es geht doch speziell ums Initialisieren, d.h. in der einfachen Zeile von oben

    foo f2 = f
    

    könnt man doch vermuten, dass f2 schon existiert, wenn es f zugewiesen bekommt, d.h. analog zu

    foof f2;
    f2 = f;
    

    sei. Ist es aber nicht. 🙂
    Scheinbar ist der Initailisierungprozess (oder wie man das nennen will) stärker, d.h. die obige Gleichung ist analog zu

    foo f2(f);
    

    Und das ist anscheinend per Konvention einfach so.

    Oder es war eine Designentscheidung oder es ist einfach logisch, das die obige einfache Gleichung zwingend den Kopierkonstruktor erfordert. Aber dazu kenne ich eben C++ noch zu wenig, um im Detail zu verstehen was beim Initialisierungsprozess geschieht.

    Gruß,
    -- Klaus.



  • Okay,

    also dieser Standardkonstruktor hat mir einfach keine Ruhe gelassen. Ganz einfach weil ich zum Thema Vererbung z.B. wieder gelesen habe

    Ist in der Basisklasse der Standardkonstruktor vorhanden, so braucht sich die abgeleitete Klasse nicht um die Initialisierung der geerbten Elemente zu kümmern [..]

    Und ich dachte mir nach der ersten Antwort dieses Posts: Warum sollte ich überhaupt einen Standardkonstruktor definieren, wenn er eh nichts tut und vom Compiler automatisch erzeugt wird.
    Dann hätte eine Basisklasse niemals einen Standarkonstruktor, weil ich mir es immer spare ihn aufzuschreiben!

    Aber der kleine aber feine Unterschied ist die Initialisierungsliste! Ein Standardkonstruktor darf keine Parameter haben und es darf nichts in seinem Rumpf geschehen, aber ich darf über die Initialisierungsliste Variablen (standardmäßig) inititialiseren. D.h. folgendes Beispiel wäre eine Standardkonstruktor

    struct foo
    {
        foo():a(0),b(0){}
        int a;
        int b;
    };
    

    Ein Standarkonstruktor kann also jede Menge tun, nämlich die Variablen initialisieren - aber er darf keine Parameter haben und es darf nichts im Rumpf passieren.

    Jetzt habe ich es doch, oder? 🙂

    Gruß,
    -- Klaus.



  • Doch, es darf etwas im Rumpf geschehen.
    Und definierst du überhaupt keine eigenen Konstruktoren, erstellt der Compiler für dich den Defaultkonstruktor, der tut wirklich ncihts, außer die Defaultkonstruktoren der Member aufzurufen.



  • Klaus82 schrieb:

    Und ich dachte mir nach der ersten Antwort dieses Posts: Warum sollte ich überhaupt einen Standardkonstruktor definieren, wenn er eh nichts tut und vom Compiler automatisch erzeugt wird.

    Weil dein Standardkonstruktor vielleicht was tun will, was der vom Compiler generierte nicht tut.

    Da brauchst du gar nicht mit Initialisiererlisten oder Vererbung ankommen.

    Aber der kleine aber feine Unterschied ist die Initialisierungsliste! Ein Standardkonstruktor darf keine Parameter haben und es darf nichts in seinem Rumpf geschehen, aber ich darf über die Initialisierungsliste Variablen (standardmäßig) inititialiseren.

    Blödsinn, natürlich darf im Rumpf was geschehen. Ein Standardkonstruktor definiert sich -- das hat hier auch schonmal jemand geschrieben -- nur dadurch, dass er ohne Argumente aufgerufen werden kann.

    Eigentlich heißt das Teil auch nicht Standardkonstruktor, sondern Defaultkonstruktor, aber Default ist halt kein deutsches Wort...



  • Okay,
    ich glaube ich verstehe es langsam. Ich habe damit ein wenig herumgespielt und bin bei folgendem etwas verwirrt, wenn bar eine Instanz von foo bekommt:

    #include <iostream>
    
    using namespace std;
    
    struct foo
    {
    	foo();
    	foo(const foo&);
    	int var;
    };
    
    foo::foo():var(5){ cout << "foo defaultconstructor called!" << endl;	}
    
    foo::foo(const foo& other):var(other.var){ cout << "foo copy constructor called!" << endl; }
    
    struct bar
    {
    	bar(foo);
    	foo f;
    	int var;
    };
    
    bar::bar(foo f):f(f){	cout << "bar constructor called" << endl;}
    
    int main()
    {
    	foo f;
    
    	foo f1(f);
    
    	bar b(f);
    
    return 0;
    }
    

    Denn die Ausgabe ist

    foo defaultconstructor called!
    foo copy constructor called!
    foo copy constructor called!
    foo copy constructor called!
    bar constructor called
    

    Zunächst wird in Zeile 27 die Instanz f mittels des Defaultconstructors initialisiert. Dann wird in Zeile 29 eine zweite Instanz f2 durch f erzeugt, das realisiert der Kopierkonstruktor.
    Jetzt verstehe ich aber nicht warum zwei weitere Male der Kopierkonstruktor aufgerufen wird. In Zeile 31 erzeuge ich doch in bar eine Instanz bar.foo durch das Hineinkopieren von f. Also würde ich erwarten, dass nur ein Mal der Kopierkonstruktor aufgerufen wird.
    Oder liegt es daran, dass in der Deklaration des Konstruktors von bar (Zeile 18 bzw. 23) zunächst eine lokale Kopie von foo erzeugt wird? Falls ja, warum wird dazu nicht der Defaultkonstruktor verwendet, sondern auch der Kopierkonstruktor?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Okay,
    ich glaube ich verstehe es langsam. Ich habe damit ein wenig herumgespielt und bin bei folgendem etwas verwirrt, wenn bar eine Instanz von foo bekommt:

    #include <iostream>
    
    using namespace std;
    
    struct foo
    {
    	foo();
    	foo(const foo&);
    	int var;
    };
    
    foo::foo():var(5){ cout << "foo defaultconstructor called!" << endl;	}
    
    foo::foo(const foo& other):var(other.var){ cout << "foo copy constructor called!" << endl; }
    
    struct bar
    {
    	bar(foo);
    	foo f;
    	int var;
    };
    
    bar::bar(foo f/*foo copy constructor called!*/):f(f/*foo copy constructor called!*/){	cout << "bar constructor called" << endl;}
    
    int main()
    {
    	foo f /*foo defaultconstructor called!*/;
    
    	foo f1(f/*foo copy constructor called!*/);
    
    	bar b(f)/*bar constructor called*/;
    
    return 0;
    }
    


  • Klaus82 schrieb:

    Oder liegt es daran, dass in der Deklaration des Konstruktors von bar (Zeile 18 bzw. 23) zunächst eine lokale Kopie von foo erzeugt wird? Falls ja, warum wird dazu nicht der Defaultkonstruktor verwendet, sondern auch der Kopierkonstruktor

    😕 Das würde doch keinen Sinn machen. Dann wäre der Parameter ja immer ein Defaultobjekt. call-by-value ➡ Kopie erzeugen.



  • out schrieb:

    😕 Das würde doch keinen Sinn machen. Dann wäre der Parameter ja immer ein Defaultobjekt. call-by-value ➡ Kopie erzeugen.

    Mh, jetzt wo du es sagst ...

    Aber ist das nicht wieder ziemlich umständlich? In Zeile 23 erzeugt der Konstruktor von bar zunächst eine Instanz von foo und kopiert den Inhalt dessen hinein was er übergeben bekommt: Kopierkonstruktor.

    Anschließend in der Initialisierungsliste wird die Instanz bar.f erzeugt und bekommt den Inhalt hineinkopiert, den der Parameter f hat: Kopierkonstruktor.

    Eigentlich will dich doch direkt den übergebenen Inhalt in bar.f hineinkopieren?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Aber ist das nicht wieder ziemlich umständlich? In Zeile 23 erzeugt der Konstruktor von bar zunächst eine Instanz von foo und kopiert den Inhalt dessen hinein was er übergeben bekommt: Kopierkonstruktor.

    Anschließend in der Initialisierungsliste wird die Instanz bar.f erzeugt und bekommt den Inhalt hineinkopiert, den der Parameter f hat: Kopierkonstruktor.

    Eigentlich will dich doch direkt den übergebenen Inhalt in bar.f hineinkopieren?

    Darum nimmt man dafür ja auch call-by-reference. const foo& f



  • out schrieb:

    Darum nimmt man dafür ja auch call-by-reference. const foo& f

    Gut, genau das hatte ich mir auch als Antwort bzw. Grund überlegt! 🙂

    Edit:
    Danke für die ausführliche und geduldige Hilfe. 🙂

    Viele Grüße,
    -- Klaus.


Anmelden zum Antworten