Shallow und Deep Copying



  • Huhu

    Ich lese gerade das Kapitel "Classes and dynamic memory allocation".

    Da lernt man, wie man mit Pointern umgeht, die innerhalb von Klassen erzeugt werden. (Soweit ich das verstanden habe, gehören die, wenns ums löschen geht, nicht zu einem Objekt).

    Das Hauptproblem ist ja, dass C++ z.B. beim Zuweisen (assignment) nur ein shallow-copying macht, wodurch einfach nur die Adresse eines Pointers übergeben wird, der Pointer des neuen Objekts also nicht mit new erzeugt wird und somit eigenständig ist.

    Wenn man dies nicht mit den entsprechenen Konstruktoren handhabt, gibt das grosse Probleme, wie im Buch hier sehr schön demonstriert wird.

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

    PS:
    Nein, ich will keine Diskussion starten, in der gestritten wird, ob C++ schlecht ist und was besser ist oder so ^^
    Ich kann mir nur gerade nicht vorstellen, wiso dass es so geregelt ist und ich möchte das gerne verstehen.



  • icarus2 schrieb:

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

    Weil es ziemlicher Mist wäre, wenn du tatsächlich bei ner Kopie deines Objekts den Pointer haben willst, und kein neues Objekt.
    Für eigene Wünsche gibt es eben assignment operator (operator=) und Copy-Konstruktor.



  • Hehe, ich hatte mir noch gedacht, dass es damit zu tun haben könnte.

    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?



  • Nö, wäre nicht möglich gewesen. Ein Pointer ist auch ein Typ. Sein Wert ist eine Adresse, und zwar die des Objektes auf das er zeigt. Beim Kopieren aller Member wird auch ein Pointer kopiert. Für einen Pointer bedeutet dies, dass die Adresse kopiert wird. Der Pointer selber ist dann ein neues Objekt!

    Wenn du das Verhalten nicht wünschst, sondern beim Kopieren eines Objektes sämtliche Member kopiert bekommen willst, nimm einfach keinen Pointer als Member 🙂



  • Nur ein Programmierer kann wissen, ob ein Zeiger bzw. eine Referenz intern ein Deep-Copy erfordert.
    Die Definition 'char *s' sagt dem Compiler nichts über den Inhalt bzw. die Speicherallokation...



  • Achso, jetzt ist alles klar wiso das nicht geht 🙂

    Thx für die Antworten.



  • C++ macht gewissermassen immer ein deep copy. Es kopiert immer die Klasse, so wie sie ist. Ein int wird kopiert, ein double wird kopiert oder ein Zeiger wird kopiert. Damit hat die neue Klasse in seinem Zeigermember den selben Wert, wie die ursprüngliche Klasse, das heisst, sie Zeigt wie die ursprüngliche Klasse auf das selbe Objekt.

    Ein Zeiger ist nun mal nicht das Objekt, auf den verwiesen wird, sondern der Zeiger ist eine Speicheradresse. Wenn es semantisch anders gemeint ist, also das verzeigerte Objekt eigentlich der Klasse gehören soll, dann muss man das halt so programmieren. Im grunde verhält sich C++ hier wie viele andere Sprachen.

    In C++ gibt es halt einen Unterschied zwischen Zeiger und Objekt. In anderen Programmiersprachen wird das nicht so unterschieden. Ich sehe es als Feature. Aber ich bin halt sicher durch die vielen Jahre C++-Erfahrung auch ein wenig voreingenommen 😉 .



  • 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.


Anmelden zum Antworten