Shallow und Deep Copying
-
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_ptrder 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_ptrder 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 Basisklassehelper_base<T>abgeleitet ist. Die Basisklasse bietet abstrakte Funktionen fürs Kopieren an, die abgeleitete implementiert diese Funktionen für den konkreten TypDerived(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 inhelper_base<T>erreicht werden. Die einzige Bedingung dafür ist, dass zur Initialisierungszeit der statische Typ bekannt ist, was bei einer Konstruktion mitnewnie ein Problem darstellt. Das selbe Prinzip kann für eine spätere Zuweisung überreset()angewandt werden.Die ganze Klasse beinhaltet natürlich ein wenig Overhead, aber angesichts der Tatsache, dass
Tkeine virtuelleClone()-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ß,
SPPS: 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 #endifDazu 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_mathMeiner 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 errorfunktioniert 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
