Shallow und Deep Copying



  • Ja, es ist ein Feature, das man wahrscheinlich in einigen Situationen sehr gut gebrauchen kann.

    Zum lernen ist es am Anfang etwas kompliziert und verwirrend. Vor allem kann man sehr leicht Fehler machen wenn man sich nicht wirklich gut auskennt ^^



  • icarus2 schrieb:

    Vor allem kann man sehr leicht Fehler machen wenn man sich nicht wirklich gut auskennt ^^

    Das ist sowieso eine Eigenschaft von C++. Dafür hast du enorm mächtige Möglichkeiten, wenn du dich auskennst. 😉

    icarus2 schrieb:

    Aber wäre es theoretisch(soll nicht heissen, dass es sinnvoll ist) möglich gewesen, das ganze umzukehren? Sprich man macht immer eine deep-copy und wenn man das nicht will, muss man es selber anspassen?

    Technisch wäre es vielleicht möglich gewesen, jeden Zeiger tief zu kopieren. Aber so eine Regelung wäre jenseits von Gut und Böse. Wie will man da kompatibel zu PODs aus C bleiben? In sehr vielen Fällen sind Zeiger nur Verweise ohne Besitz. Was sollte passieren, wenn das Objekt, auf das verwiesen wird, nicht kopierbar ist?

    Andererseits stimmt es auch nicht, dass du jedes Mal die Grossen Drei selbst implementieren musst, wenn du Zeiger tief kopieren willst. Für solche Anwendungsfälle gibt es Smart-Pointer mit den unterschiedlichsten Kopiersemantiken.

    Und ja, ich kann mir immer noch nicht erklären, wieso weder die Standardbibliothek noch Boost einen kopierenden Smart-Pointer anbieten. Als wäre der sowieso überbewertete shared_ptr der Weisheit letzter Schluss. 🙄
    (Wenigstens Loki machts richtig.)



  • Jo, wenn man sich auskennt (ist bei mir irgendwie nicht so der Fall ^^ sollte aber irgendwann noch kommen).

    Smart pointers kenne ich noch nicht, aber vielleicht wird das hier im Buch ja noch behandelt.

    Thx



  • Nexus schrieb:

    Und ja, ich kann mir immer noch nicht erklären, wieso weder die Standardbibliothek noch Boost einen kopierenden Smart-Pointer anbieten. Als wäre der sowieso überbewertete shared_ptr der Weisheit letzter Schluss. 🙄
    (Wenigstens Loki machts richtig.)

    Wozu sollte der gut sein? Wenn ich eine Klasse in alleinigen Besitz haben möchte, brauche ich keinen Zeiger mehr - auch keinen smarten. Ich bevorzuge einfach auf Zeiger soweit wie möglich zu verzichten. Smart-Pointer sind nur die zweitbeste Alternative.

    Die Frage ist übrigens nicht rhetorisch gedacht, sondern mich würden wirklich Anwendungsfälle interessieren, wo Du so einen kopierenden Smart-Pointer gebrauchen könntest.

    Auch würde mich interessieren, wie so einer aussehen könnte. Ein Zeiger - auch ein smarter Zeiger - kann auf eine abgeleitete Klasse zeigen und die vollständig zu kopieren ist meiner Meinung nach nicht allgemeingültig lösbar.



  • tntnet schrieb:

    Wozu sollte der gut sein? Wenn ich eine Klasse in alleinigen Besitz haben möchte, brauche ich keinen Zeiger mehr - auch keinen smarten. Ich bevorzuge einfach auf Zeiger soweit wie möglich zu verzichten. Smart-Pointer sind nur die zweitbeste Alternative.

    Er hat zwei nicht zu unterschätzende Vorteile.

    • Gegenüber Zeigern: Der kopierende Smart-Pointer besitzt Value-Semantik, wie man sie von normalen Objekten her kennt (Kopierbarkeit, Zuweisbarkeit und solche Sachen). Kein geteilter Besitz oder so ein Spezialfall. Implementierung der Big Three ist nicht nötig.
    • Gegenüber "normalen" Value-Objekten: Der gehaltene Typ muss bei der Deklaration nicht bekannt sein. Wahnsinnig praktisch, wenns ums Verringern von Abhängigkeiten geht.
    • Natürlich gibt es auch Nachteile, wie die zusätzliche Heap-Allokation. Aber bei rohen Zeigern hat man dieses Problem auch.

    tntnet schrieb:

    Auch würde mich interessieren, wie so einer aussehen könnte. Ein Zeiger - auch ein smarter Zeiger - kann auf eine abgeleitete Klasse zeigen und die vollständig zu kopieren ist meiner Meinung nach nicht allgemeingültig lösbar.

    Doch, sowas ist möglich – sogar ohne Clone() . 😮

    Der Trick dabei ist, dass der Konstruktor ein Funktionstemplate ist, das den statischen Typen des erstellten Objekts erkennt. Nur wenn dieser bekannt ist, kann eine Kopie vom selben Typ erstellt werden.

    template <typename T>  // Templateparameter für Klasse
    template <typename Derived> // Templateparameter für Funktion
    clone_ptr<T>::clone_ptr(Derived* new_pointer);
    

    Eine Initialisierung kann also folgendermassen aussehen:

    // Aufruf mit folgenden Templateargumenten:
    // T       == BaseClass
    // Derived == DerivedClass
    clone_ptr<BaseClass> ptr(new DerivedClass);
    

    Der Konstruktor initialisiert ein Helper-Objekt helper<T,Derived> , das von einer polymorphen Basisklasse helper_base<T> abgeleitet ist. Die Basisklasse bietet abstrakte Funktionen fürs Kopieren an, die abgeleitete implementiert diese Funktionen für den konkreten Typ Derived (das abgeleitete Helper-Objekt ist wiederum ein Template).

    template <typename T>
    template <typename Derived>
    clone_ptr<T>::clone_ptr(Derived* new_pointer)
    : m_pointer(new_pointer) // m_pointer ist Member vom Typ T*
    , m_helper(new helper<T,Derived>()) // m_helper ist Member vom Typ helper_base<T>*
    {
    }
    

    Die Funktionalität fürs polymorphe Kopieren ist also in helper<T,Derived> ausgelagert und kann über eine virtuelle Funktion in helper_base<T> erreicht werden. Die einzige Bedingung dafür ist, dass zur Initialisierungszeit der statische Typ bekannt ist, was bei einer Konstruktion mit new nie ein Problem darstellt. Das selbe Prinzip kann für eine spätere Zuweisung über reset() angewandt werden.

    Die ganze Klasse beinhaltet natürlich ein wenig Overhead, aber angesichts der Tatsache, dass T keine virtuelle Clone() -Methode anbieten muss (welche auch gewisse Gefahren wie Vergessen der Überschreibung mit sich brächte), und den anderen Vorzügen, ist es mir das völlig Wert. Davon abgesehen gibt es auch einige Anwendungsfälle, wo gar keine Polymorphie im Spiel ist, und ein entsprechender Smart-Pointer käme im Bezug auf Overhead einer manuellen Lösung recht nahe (er müsste nur die Big Three sinnvoll implementieren, was trivial ist).



  • icarus2 schrieb:

    Das Hauptproblem ist ja, dass C++ z.B. beim Zuweisen (assignment) nur ein shallow-copying macht, ...

    Das kann ich so natürlich nicht stehen lassen. Per Definition gibt es hier Element-weises Kopieren und Zuweisen. Ein "Problem" stellt das nur dar, wenn Du Dich bei der Wahl der Elementtypen "ungeschickt" anstellst. Und manchmal geht es nicht ohne Zeiger. Aber das sind kleine Spezialfälle, mit denen man sich eigentlich so gut wie gar nicht rumschlagen muss, wenn man Standard-Container als Elemente benutzt.

    icarus2 schrieb:

    Daher meine Frage:
    Wiso macht C++ nicht immer ein deep copying? Wiso macht es ein shallow copying?

    Was Du "shallow copying" nennst, ist die einzig sinnvolle und C-abwärtskompatible Semantik für Zeiger. Wenn Dir das nicht gefällt, nimm etwas anderes als Zeiger (das meinte ich mit "ungeschickter Wahl der Elementtypen" oben).

    Gruß,
    SP

    PS: Dü könntest auch mal eine Rechtschreibprüfung über Deine Texte laufen lassen. :p



  • Danke für die Antworten.

    Haben meine Texte so viele Fehler drin?



  • Um das Ganze zu üben habe ich mir nun mal eine kleine Klasse Triangle gebastelt. Die Klasse selber ist nicht wirklich brauchbar, aber als Übung reicht es aus.

    Ich habe folgendes header-file:

    // Triangle1.h -- Triangle class header file
    
    #ifndef CIRCLE_1_INCLUDED
    #define CIRCLE_1_INCLUDED
    
    #include <iostream>
    
    namespace my_math
    {
    	class Triangle
    	{
    	private:
    
    		char * name;
    		int name_length;
    		double width;
    		double height;
    
    	public:
    
    		// constructors and destructor
    		Triangle(const char * name, const int name_length, const int width = 0, const int height = 0);
    		Triangle(const Triangle &);			//copy constructor
    		~Triangle();
    
    		// overloaded operator functions
    		Triangle & operator=(const Triangle & t); //assignment operator overloading
    		friend std::ostream & operator<<(std::ostream & os, const Triangle & t); // << operator
    
    		// member functions
    		double area();
    
    		// getters
    		char * get_name();
    		double get_width();
    		double get_height();
    
    	};
    
    } // end of namespace my_math
    
    #endif
    

    Dazu das .cpp file, das die Triangle Methoden implementiert:

    // Triangle1.cpp -- implementing Triangle methods
    
    #include "Triangle1.h" //includes <iostream>
    
    namespace my_math
    {	
    	// constructor
    	Triangle::Triangle(const char * a_name, const int a_name_length, const int width, const int height)
    	{
    		std::cout << "Constructor" << std::endl;
    		name = new char[a_name_length];
    		std::strcpy(name, a_name);
    		this->name_length = a_name_length;
    		this->width = width;
    		this->height = height;
    	}
    
    	// copy constructor
    	Triangle::Triangle(const Triangle & t)		
    	{
    		std::cout << "Copy Constructor" << std::endl;
    		name_length = t.name_length;
    		name = new char[name_length + 1];
    		std::strcpy(name, t.name);
    	}
    
    	// destructor
    	Triangle::~Triangle()
    	{
    		delete [] name;
    	}
    
    	// public class methods
    	double Triangle::area()
    	{
    		return (width * height / 2);
    	}
    
    	//overloaded operator functions
    
    	Triangle & Triangle::operator=(const Triangle & t) // assignment
    	{
    		std::cout << "Assignment function" << std::endl;
    		if(this == &t)
    		{
    			return *this;
    		}
    
    		delete [] name;
    		name_length = t.name_length;
    		name = new char[name_length + 1];
    		std::strcpy(name, t.name);
    
    		return *this;
    	}
    
    	std::ostream & operator<<(std::ostream & os, const Triangle & t) // << operator
    	{
    		os << "Name: " << t.name << ", Width: " << t.width << ", Height: " << t.height;
    		return os;
    	}
    
    	// getter functions
    
    	char * Triangle::get_name()
    	{
    		return name;
    	}
    
    	double Triangle::get_width()
    	{
    		return width;
    	}
    
    	double Triangle::get_height()
    	{
    		return height;
    	}
    } // end of namespace my_math
    

    Meiner Meinung nach müsste alles funktionieren.
    Beim Testen gibt es jedoch Fehler. Hier ein kleines Beispiel:

    // TriangleTest.cpp -- used to test the Triangle class
    
    #include <iostream>
    #include "Triangle1.h"
    
    int main()
    {
    	using std::cout;
    	using std::endl;
    	using my_math::Triangle;
    
    	Triangle *t1 = new Triangle("One", 3, 100.0, 100.0); // works
    	Triangle t2 = Triangle("Two", 2, 20.2, 40.2);	// causes HEAP CORRUPTION DETECTED error
    
    	delete t1;		// causes HEAP CORRUPTION DETECTED error
    
    	return 0;
    }
    

    Kann mir jemand sagen, was ich in der Triangle-Klasse ändern muss, so dass der Code funktioniert? Aus meiner Sicht müsste alles korrekt ablaufen. Aber das tut es leider nicht.

    PS:
    Ich weiss, dass das char * nicht wirklich toll ist und ich wohl besser einfach string verwenden würde. Ich würde es normalerweise auch anders machen. Aber ich brauchte ja einen Pointer, der mit new erzeugt wird, in der Übung.



  • Der String "Two" hat 3 Zeichen. Somit überschreitet Dein strcpy den allokierten Puffer, da Du als Länge nur 2 übergibst.



  • Benutze doch strlen(..) um die Länge zu ermitteln.
    Simon



  • Thx. Das hatte ich ganz übersehen 🙄

    Aber auch mit dem hier:

    Triangle t2 = Triangle("Two", 3, 20.2, 40.2);	// causes HEAP CORRUPTION DETECTED error
    

    funktioniert es nicht.



  • tntnet schrieb:

    Der String "Two" hat 3 Zeichen.

    Irrtum, es sind vier. Und hast du meinen letzten Beitrag schon gelesen? 😉

    icarus2 schrieb:

    Thx. Aber auch mit dem hier [..] funktioniert es nicht.

    Wie sieht denn dein Konstruktor aus? Immer noch gleich? Schau, dass du die Nullterminierung bei der Speicheranforderung berücksichtigst. Aber warum überhaupt das ganze Rumgefrickel mit C-Strings? Kennst du std::string ?



  • Nexus, du bist der Beste 😉

    Ich hatte vergessen die Nullterminierung zu berücksichtigen. Jetzt klappt auch alles.

    Ich kenne std::string. Allerdings habe ich das hier absichtlich mit char * gemacht, da ich einen pointer in der Klasse haben wollte, um das ganze mal ein bisschen zu üben. Steht irgendwo in meinem Post oben. Ich benutze sonst schon std::string.

    Dangööö 🙂

    *Edit
    Ja, der Konstruktor ist immer noch gleich.



  • icarus2 schrieb:

    ...
    Das Hauptproblem ist ja, ...

    Das Hauptproblem wovon?

    icarus2 schrieb:

    ...
    Wiso macht C++ nicht immer ein deep copying? ...

    Ich halte Deine Grundthese für falsch. C++ macht nicht "immer eine shallow copy", sondern kopiert eben genau das, was es in der Hand hat: Einen string, ein Objekt, .... oder eben einen Zeiger.
    Ein Zeiger ist eben nicht dasselbe wie das Objekt, auf das er zeigt. Es ist ein Verweis.

    Mal umgedreht (Einmal angenommen, C++ würde bei Pointerkopie automatisch ein neues Objekt erstellen) - was würde bei folgendem Code passieren?

    MyObject m;
    MyObject* p1(&m);
    MyObject* p2(&m);
    if(p1 == p2) cout << "was wir erwarten würden\n";
    else cout << "Hoppla\n";
    
    p2 = p1; // Nach Deiner Theorie müsste hier ein 
    // neues Objekt erzeugt werden, auf das p2 dann zeigt.
    
    if(p1 == p2) cout << "was wir erwarten würden\n";
    else cout << "Hoppla\n";
    // hier müsste nun "Hoppla" rauskommen, weil p1 und p2 
    // nicht mehr auf dasselbe Objekt zeigen.
    

    Das wäre extremer Quark.
    (Genaugenommen wäre es wohl schon unmöglich, überhaupt einem Zeiger die Adresse eines Objekts zuzuweisen ... weil ja jedesmal ein neues Objekt angelegt würde)

    Gruß,

    Simon2.



  • Das Hauptproblem wovon?

    Naja, Problem ist wohl etwas das falsche Wort. Besser gesagt, die Tatsache, dass man in gewissen Fällen den Copy Constructor usw. überschreiben muss. (Siehe Code-Beispiele von mir).

    Ich halte Deine Grundthese für falsch. C++ macht nicht "immer eine shallow copy", sondern kopiert eben genau das, was es in der Hand hat: Einen string, ein Objekt, .... oder eben einen Zeiger.

    Also wenn ich das richtig verstanden habe wurde im C++ Primer gesagt, dass C++ ein shallow copying macht (zumindest in den Beispielen, um die es in meinen Beispielen geht). So wie ich das verstanden habe ist es ein shallow copying, weil nur die Adresse übergeben wird. Der neue Pointer wird ja nicht mit new erzeugt oder liege ich da falsch? (hoffe, ich konnte mich verständlich ausdrücken ^^).
    Ich habe nocht nicht so den Überblick und wollte nur verstehen, wiso es von den C++ Entwicklern so gemacht wurde.



  • icarus2 schrieb:

    ...Besser gesagt, die Tatsache, dass man in gewissen Fällen den Copy Constructor usw. überschreiben muss. ...

    Nunja, man muss eben genau da einen CopyCtor o.ä. bereitstellen, wo es eben nicht eindeutig ist, wie kopiert werden soll.
    In vielen Fällen ist es das aber.

    Was der Primer nun genau damit meint, dass "C++ ein shallow copying macht", weiß ich nicht. Aber es sollte mich sehr wundern, wenn dort stünde, dass "C++ IMMER NUR ein shallow copying macht", weil das einfach nicht stimmt.
    Vermutlich geht es da eher um einen speziellen Fall.

    Nur mal zur Erklärung:

    icarus2 schrieb:

    ...Der neue Pointer wird ja nicht mit new erzeugt ...

    Da meinst Du bestimmt: "...Das neue Objekt wird nicht mit new erzeugt...". ... und ich glaube, der Unterschied ist schon ein wichtiger Auslöser für Deine Verwirrung.
    Ein Zeiger auf ein Objekt ist eben etwas ganz Anderes als das Objekt selbst (So wie eine Hausanschrift etwas ganz Anderes ist als ein Haus).

    Außerdem scheinst Du "Zeiger" mit "dynamischer Objekterzeugung" zu vermengen. Letzteres ist aber nur ein spezieller (und kleiner) Anwendungsbereich von Zeigern. Bei dynamischer Objekterzeugung sagst Du als Programmierer: "Ich will irgendwann zur Laufzeit selbst bestimmen, ob und wann und wie ich ein Objekt erzeugen will" - und dass Du in dem Fall das dann auch machen musst (z.B. durch einen selbstgeschriebenen CopyCtor) , ist IMHO weder verwunder- noch verwerflich. 😉
    Bei Java ist das übrigens nicht anders.
    Mal so gesagt: Kein Compiler der Welt kann von sich aus (ohne, dass Du es ihm sagst) wissen, ob Du
    a) ein neues Objekt haben möchtest oder
    b) ob Dein Zeiger auf ein bereits bestehendes zeigen soll.

    Zusammenfassung: Es gibt eine Menge Anwendungsfälle von Zeigern und man muss sich nur um das Kopieren kümmern, wenn man nicht den "Standardweg" gehen möchte.
    Deshalb ist es sinnlos und nicht möglich, diese Arbeit über eine "automatischer deep copy bei Zeigern" abzuwickeln.

    Gruß,

    Simon2.



  • Ja, dann war das "shallow copying" wohl nur auf einen Anwendungsfall bezogen und ich habe das nicht ganz mitbekommen.

    Deine Erklärung hat mir sehr geholfen. Ich glaube ich sollte das Ganze jetzt besser verstehen.

    Vielen Dank 🙂



  • icarus2 schrieb:

    Ja, dann war das "shallow copying" wohl nur auf einen Anwendungsfall bezogen und ich habe das nicht ganz mitbekommen.

    Nein, "shallow copying" wird als ein synonymer Begriff für "memberwise copying" verwendet und auf den implizit erzeugten Copy-Konstruktor bezogen. D.h. die Aussage ist völlig korrekt und steht fast 1:1 so im Standard.

    Nebenbei, wenn du schon zu einem C++ Primer greifst, dann doch lieber zum Buch von Lippman. Der ist die wesentlich kompetentere Kapazität auf diesem Gebiet. Obwohl man zugeben muss, dass Stephen Prata nicht unbedingt ein schlechter Autor ist.



  • Nein, "shallow copying" wird als ein synonymer Begriff für "memberwise copying" verwendet und auf den implizit erzeugten Copy-Konstruktor bezogen. D.h. die Aussage ist völlig korrekt und steht fast 1:1 so im Standard.

    Ok, jetzt steht Aussage gegen Aussage ^^

    Nebenbei, wenn du schon zu einem C++ Primer greifst, dann doch lieber zum Buch von Lippman. Der ist die wesentlich kompetentere Kapazität auf diesem Gebiet. Obwohl man zugeben muss, dass Stephen Prata nicht unbedingt ein schlechter Autor ist.

    Bin mit dem C++ Primer Plus eigentlich sehr zufrieden.

    Btw... schicker Name 😉



  • Mitleid schrieb:

    ...Nein, "shallow copying" wird als ein synonymer Begriff für "memberwise copying" verwendet und auf den implizit erzeugten Copy-Konstruktor bezogen....

    😕
    Habe ich so noch nicht gehört (was nicht viel bedeutet)....
    Was ist denn dann das Gegenteil von "shallow copying"?

    Gruß,

    Simon2.


Anmelden zum Antworten