Virtueller Zuweisungsoperator
-
fr33g schrieb:
So hab mir das durchgelesen und kompiliert, aber irgendwie schocken mich die Ergebnise grad:D, kannst du vll kurz mal dazu ne kleine Beschreibung einfügen, wann was warum ausgeführt wird.
/* Beispiel 1 von "mgaeckler" */ #include <iostream> using namespace std; class base { public: base &operator = ( const base &src ); }; class derived : public base { public: int derivedMem; derived &operator = ( const base &src ); }; base &base::operator = ( const base &src ) { cout << "base" << endl; return *this; } derived &derived::operator = ( const base &src ) { cout << "sub" << endl; this->derivedMem = 0; return *this; } int main() { base *ptr2 = new base; // ptr2 : base-Zeiger auf base-Objekt derived *ptr4 = new derived; // ptr4 : sub-Zeiger auf sub-Objekt derived *ptr5 = new derived; // ptr5 : sub-Zeiger auf sub-Objekt ptr5->derivedMem = 3; ptr4->derivedMem = 2; cout << ptr5->derivedMem << endl << endl; // Ausgabe 3 /* derived::operator=(const derived&) wird aufgerufen [ ptr5->derivedMem ist nun 2 ], die dann base::operator=(const base&) auruft. */ *ptr5 = *ptr4; cout << ptr5->derivedMem << endl << endl; // Ausgabe 2 /* derived::operator=(const base&) wird aufgerufen [ ptr5->derivedMem ist nun 0 ]. */ *ptr5 = static_cast<base>(*ptr4); cout << ptr5->derivedMem << endl << endl; // Ausgabe 0 /* derived::operator=(const base&) wird aufgerufen [ ptr5->derivedMem ist nun 0 ]. */ *ptr5 = *ptr2; cout << ptr5->derivedMem << endl; // Ausgabe 0 system("pause>nul"); return 0; }
-
/* Beispiel 2 von "mgaeckler" */ #include <iostream> using namespace std; class base { public: int baseMem; virtual base &operator = ( const base &src ); }; class derived : public base { public: int derivedMem; base &operator = ( const base &src ); }; base &base::operator = ( const base &src ) { cout << "base" << endl; this->baseMem = src.baseMem; return *this; } base &derived::operator = ( const base &src ) { cout << "sub" << endl; this->baseMem = src.baseMem; const derived *dSrc = dynamic_cast<const derived *>(&src); // dynamic cast möglich, da die Klasse polymorph ist. if( dSrc ) { cout << "ok" << endl; this->derivedMem = dSrc->derivedMem; } else { cout << "failed" << endl; this->derivedMem = 0; } return *this; } int main() { base *ptr1 = new base; // ptr1 : base-Zeiger auf base-Objekt derived *ptr2 = new derived; // ptr2 : sub-Zeiger auf sub-Objekt derived *ptr3 = new derived; // ptr3 : sub-Zeiger auf sub-Objekt base *ptr4 = ptr3; // ptr4 : base-Zeiger auf sub-Objekt ptr2->derivedMem = 3; ptr3->derivedMem = 2; cout << ptr3->derivedMem << endl << endl; // Ausgabe 2 /* derived::operator=(const derived&) wird aufgerufen [ ptr3->derivedMem ist nun 3], die dann base::operator=(const base&) auruft. */ *ptr3 = *ptr2; cout << ptr3->derivedMem << endl << endl; // Ausgabe base + 3 /* derived::operator=(const base&) wird aufgerufen. [ ptr3->derivedMem ist nun 0, da dynamic cast fehlschlägt, da "src" vom Typ "base" ist. --> Zeiger können in der Hirarchie nur nach unten zeigen, und nicht nach oben. ] */ *ptr3 = *ptr1; cout << ptr3->derivedMem << endl << endl; // Ausgabe sub + failed + 0 /* Hier kommt nun "virtual" zum Tragen: Der Typ des Objekts, auf das ptr4 zeigt, entscheidet. --> derived Wieso aber derived::operator=(const base&) aufgerufen wird, und nicht derived::operator=(const derived&), da *ptr2 ja vom Typ "derived" ist, kann ich nicht nachvollziehen. ????? */ *ptr4 = *ptr2; cout << ((derived*)ptr4)->derivedMem << endl << endl; // ????? /* Hier kommt nun "virtual" zum Tragen: Der Typ des Objekts, auf das ptr4 zeigt, entscheidet. --> derived derived::operator=(const base&) wird aufgerufen. [ ptr4->derivedMem ist nun 0, da dynamic cast fehlschlägt, da "src" vom Typ "base" ist. --> Zeiger können in der Hirarchie nur nach unten zeigen, und nicht nach oben. ] */ *ptr4 = *ptr1; cout << ((derived*)ptr4)->derivedMem << endl; // Ausgabe sub + failed + 0 system("pause>nul"); return 0; }
Vielleicht kann einer hierzu
/* Hier kommt nun "virtual" zum Tragen: Der Typ des Objekts, auf das ptr4 zeigt, entscheidet. --> derived Wieso aber derived::operator=(const base&) aufgerufen wird, und nicht derived::operator=(const derived&), da *ptr2 ja vom Typ "derived" ist, kann ich nicht nachvollziehen. ????? */ *ptr4 = *ptr2; cout << ((derived*)ptr4)->derivedMem << endl << endl; // ?????
Stellung nehmen
-
Hey erst mal vielen Dank, jetzt versteh ich das ganze langsam endlich:D
Aber die eine Ausgabe verwundert mich auch, wieso dort nicht der compilergenerierte Zuweisungsoperator aufgerufen wird, sondern der mit base als parameter.Wär cool wenn da vielleicht jemand was zu sagen könnte.
Auf jeden Fall schonmal Vielen Dank
Gruß freeG
-
Jetzt wird's strange
#include <iostream> using namespace std; class base { public: virtual base &operator = ( const base &src ) {}; }; class derived : public base { public: virtual derived &operator = ( const base &src ); virtual derived &operator = ( const derived &src ); }; derived &derived::operator = ( const base &src ) { cout << "sub" << endl; } derived &derived::operator = ( const derived &src ) { cout << "ddddddddddddddddddddddddddd" << endl; } int main() { base *ptr2 = new derived; base *ptr4 = new derived; /* error: no matching function for call to derived::derived(base&) candidates are: derived::derived() derived::derived(const derived&) */ (*ptr4).operator=( (derived)(*ptr2) ); //*ptr4 = (derived)(*ptr2); /* Geht, obwohl es doch dasselbe ist wie oben oO. */ (*ptr4).operator=(*ptr2); system("pause>nul"); return 0; }
-
Es wurde in diesem und im von mir verlinkten Thread schon genügend oft gesagt, dass ein virtueller Zuweisungsoperator nicht sehr sinnvoll ist. Ihn mittels
dynamic_cast
zu implementieren, um halbwegs Double-Dispatch möglich zu machen, finde ich ziemlich gehackt. Da gibt es weitaus bessere Ansätze, z.B. in Alexandrescus "Modern C++ Design".Das Problem ist, dass Wertsemantik (und damit Dinge wie Kopierkonstruktor oder Zuweisungsoperator) nicht direkt polymorph implementiert werden kann, weil die Objekte oft inkompatibel sind und man Slicing oder sonstiges undefiniertes Verhalten leichtfertig in Kauf nimmt, wenn man versucht, gleich wie bei statischen Typen vorzugehen.
Das heisst nicht, dass polymorphe Klassen generell keine Kopierkonstruktoren und Zuweisungsoperatoren haben dürfen, jedoch sollten diese nicht virtuell sein. Beim Konstruktor ist bereits durch die Sprache gegeben, dass der dynamische Typ des neuen Objekts bekannt sein muss, beim Zuweisungsoperator jedoch nicht, was zu solchen Experimenten verleitet. Wertsemantik ist sehr wohl möglich, aber nur auf gleicher Ebene und mit statischen Typen. Für "polymorphe Wertsemantik" stellen immer noch Smart-Pointer eine Möglichkeit dar, welche z.B. eine virtuelle
Clone()
-Funktion aufrufen und sicherstellen, dass kein Slicing zum Tragen kommt.
-
Dweb schrieb:
Vielleicht kann einer hierzu
/* Hier kommt nun "virtual" zum Tragen: Der Typ des Objekts, auf das ptr4 zeigt, entscheidet. --> derived Wieso aber derived::operator=(const base&) aufgerufen wird, und nicht derived::operator=(const derived&), da *ptr2 ja vom Typ "derived" ist, kann ich nicht nachvollziehen. ????? */ *ptr4 = *ptr2; cout << ((derived*)ptr4)->derivedMem << endl << endl; // ?????
Stellung nehmen
Du erwartest von virtuellen Funktionen Dinge, die sie schlichtweg nicht können.
Polymorphie ist nur möglich, wenn die Signatur der Funktion in abgeleiteten und Basisklassen gleich ist. Die Parametertypen dürfen nicht kovariant sein. Wenn somit eine virtuelle Funktion
virtual base& base::operator= (const base&);
aufgerufen wird, kommt für linke Operanden vom dynamischen Typ
derived
nur der Zuweisungsoperatorvirtual derived& derived::operator= (const base&);
in Frage.
Das habe ich übrigens in meinem Post erklärt. Schade, dass er nicht gelesen wurde.
Nexus schrieb:
Nebenbei: Ein virtueller Zuweisungsoperator ist ein wenig fragwürdig. Und zwar einfach, weil C++ kein Double-Dispatching kann und der rechte Operand somit als statischer Typ interpretiert wird [...]
Dweb schrieb:
Jetzt wird's strange
[...] derived &derived::operator = ( const base &src ) { cout << "sub" << endl; } derived &derived::operator = ( const derived &src ) { cout << "ddddddddddddddddddddddddddd" << endl; }
Was hast du für einen Compiler, der das Fehlen des Rückgabewertes toleriert? Wenn nicht mal richtig kompiliert wird, wäre ich erst recht vorsichtig mit Interpretationen des Laufzeitverhaltens.
-
Nexus schrieb:
Das habe ich übrigens in meinem Post erklärt. Schade, dass er nicht gelesen wurde.
Nexus schrieb:
Nebenbei: Ein virtueller Zuweisungsoperator ist ein wenig fragwürdig. Und zwar einfach, weil C++ kein Double-Dispatching kann und der rechte Operand somit als statischer Typ interpretiert wird [...]
Ich habe das gelesen Nexus, aber in dem Beispiel ist der rechte Operand als statischer doch vom Typ der abgeleiteten Klasse, oder überseh ich da was?
Gruß freeG
-
fr33g schrieb:
Ich habe das gelesen Nexus, aber in dem Beispiel ist der rechte Operand als statischer doch vom Typ der abgeleiteten Klasse, oder überseh ich da was?
Nein, der statische Typ ist eben die Basisklasse.
base* dst = new derived; derived* src = new derived; *dst = *src;
Ohne
virtual
würde folgende Funktion aufgerufen werden.base& base::operator= (const base&);
Da nun aber virtuelle Funktionen im Spiel sind, wird der linke Operand (das Objekt
*this
) dynamisch dispatcht. Aufgerufen wird also die überschriebene Methodevirtual derived& derived::operator= (const base&);
wie im letzten Post erklärt. Es gibt keinen Grund, wieso
virtual derived& derived::operator= (const derived&);
aufgerufen werden sollte, da diese Funktion erst in der abgeleiteten Klasse deklariert ist. Bei der Zuweisung wird jedoch aufgrund des statischen Typen
base
in der Basisklasse nach Überladungen gesucht.Aber wie gesagt: Meide virtuelle Zuweisungsoperatoren, denn sie sind ein fehleranfälliger Workaround, um nicht vorhandene Wertsemantik und Double-Dispatching im polymorphen Kontext zu lösen.
-
Dweb schrieb:
Vielleicht kann einer hierzu
/* Hier kommt nun "virtual" zum Tragen: Der Typ des Objekts, auf das ptr4 zeigt, entscheidet. --> derived Wieso aber derived::operator=(const base&) aufgerufen wird, und nicht derived::operator=(const derived&), da *ptr2 ja vom Typ "derived" ist, kann ich nicht nachvollziehen. ????? */ *ptr4 = *ptr2; cout << ((derived*)ptr4)->derivedMem << endl << endl; // ?????
Stellung nehmen
Ist doch einfach. ptr4 ist vom Typ base* also kann er nur Funktionen, die in base deklariert sind, aufrufen. Wenn sie virtuell sind, dürfen sie zwar auch in abgeleiteten Klassen definiert sein, sie müssen aber in jedem Fall in der Basisklasse deklariert sein.
derived::operator=(const derived&) ist in der Basisklasse weder definiert noch deklariert.
mfg Martin
-
mgaeckler schrieb:
Ist doch einfach. ptr4 ist vom Typ base* also kann er nur Funktionen, die in base deklariert sind, aufrufen. Wenn sie virtuell sind, dürfen sie zwar auch in abgeleiteten Klassen definiert sein, sie müssen aber in jedem Fall in der Basisklasse deklariert sein.
derived::operator=(const derived&) ist in der Basisklasse weder definiert noch deklariert.
mfg Martin
Nexus hat's auch schon geschrieben. Aber ich gebe mal ein weiteren Hinweis:
überlegt euch mal:
Der Compiler sieht:*ptr4 = *ptr5;
Der Compiler kann nun nur an Hand des Objektes auf das ptr4 zeigt entscheiden, welche Funktion aufgerufen wird. Hierzu benutzt er eine Funktionstabelle, deren Aufbau ausschließlich von der Klasse base abhängt. Das muß deshalb so sein, weil base in jedem Kontext immer identisch sein muß. Deshalb kann base ja auch Funktionen aufrufen, von denen es nur die Deklaration aber nicht die Definition kennt. Ja sogar von abgeleiteten Klassen, die es gar nicht kennt. Das geht deshalb, weil die abgeleiteten Klassen die Struktur von base kennen und in deren Konstruktoren (oder wie auch immer der Compiler das realisiert) wird dann die Funktionstabelle entsprechend angepasset.
Für jede Klasse gibt es eine eigene Funktionstabelle und jedes Objekt enthält einen Zeiger auf diese Funktionstabelle. (Oder wie auch immer der Compiler das realisiert)
Die Funktionstabelle von base enthält nun definitiv keinen Eintrag für einen Zeiger auf
operator = ( derived & )
, weil die Deklaration der Klasse base gar keine solche Funktion kennt sonder nuroperator = ( base & )
. Wenn der Compiler die Struktur von base ermittelt betrachtet er nicht im geringsten irgendwelche abgeleiteten Klassen. Wäre auch Unfug, denn was soll er machen, wenn er die noch gar nicht kennt, weil sie in einer anderen Quelltext datei ist, oder weil sie noch gar nicht existiert?mfg Martin
-
Ein virtueller Zuweisungsoperator genügt NICHT, weil der Zuweisungsoperator anders als "normale" virtuelle Funktionen aufgerufen wird. Wenn nur ein virtueller Zuweisungsoperator geschrieben wurde, wird bei der Compilation der nicht-virtuelle automatisch auch noch erzeugt - und ggf. statt des virtuellen aufgerufen (siehe Abschnitt 13.5.3 im Standardentwurf).
Im Folgenden geht es nur darum, eine richtige und vollständige Lösung für polymorphe Zuweisung zu zeigen, nicht darum, ob polymorphe Zuweisung überhaupt sinnvoll ist.
In Kürze:
- ein virtueller Zuweisungsoperator ist nicht sinnvoll- FALLS man eine virtuelle Zuweisung möchte, ist sie am einfachsten zu realisieren, indem man den Zuweisungsoperator in jeder Klasse überschreibt und in ihm eine virtuelle Methode assign() aufruft. In dieser Methode genügt ein static_cast, weil der polymorphe Aufruf dafür sorgt, dass sowieso die richtige der überschriebenen assign()-Methoden aufgerufen wird.
- WENN Objekt-Slicing verhindert werden soll, kann man eine Prüfung mit typeid einbauen.
Der Hinweis auf bessere Lösungen in Alexandrescus "Modern C++ Design", ist insofern nicht hilfreich, weil in der dort beschriebenen Clone-Factory Zeiger zurückgegeben werden - das ist eine andere Problemstellung (natürlich trotzdem interessant). Hier geht es um Wertsemantik. Wenn die nicht gefragt ist, sollte man sich die Clone-Factory ansehen.
Im folgenden wird versucht, die polymorphe Zuweisung auf das einfachste Schema zu reduzieren, wie schon in dem von Nexus angegebenen Thread diskutiert. Unten ist ein vollständiges Beispiel mit einer Vererbungshierarchie bestehend aus drei Klassen A, B und C, und einem main-Programm zum Testen, der Einfachheithalber alles in einer Datei. Der Vollständigkeit halber hat jede Klasse ein int-Attribut und ein dynamisches Attribut (char*). Mit dem Makro TYPPRUEFUNG wird gesteuert, ob typeid zur Objekt-Slicing-Kontrolle eingesetzt wird.
(getestete!) Verbesserungen und Vereinfachungen?
#include<algorithm> // swap #include<cassert> #include<cstring> #include<iostream> #include<typeinfo> // zum Vergleich Makro auskommentieren #define TYPPRUEFUNG using std::cout; using std::cerr; using std::endl; class A { public: A(int a_, const char* as_) : a(a_), as(new char[strlen(as_)+1]) { strcpy(as, as_); } A(const A& rhs) : a(rhs.a), as(new char[strlen(rhs.as)+1]) { strcpy(as, rhs.as); } virtual ~A() { delete [] as; } virtual A& assign(const A& rhs) { cout << "virtual A& A::assign(const A&)" << endl; #ifdef TYPPRUEFUNG if(typeid(*this) != typeid(rhs)) { throw std::bad_typeid(); } #endif A temp(rhs); swap(temp); return *this; } A& operator=(const A& rhs) { cout << "A& operator=(const A& rhs)" << endl; return assign(rhs); } virtual void ausgabe() const { cout << "A.a = " << a << ", A.as = " << as << " "; } void swap(A& rhs) { std::swap(a, rhs.a); std::swap(as, rhs.as); } virtual bool operator==(const A& arg) const { return #ifdef TYPPRUEFUNG typeid(*this) == typeid(arg) && #endif a == arg.a && strcmp(as, arg.as) == 0; } private: int a; char* as; }; class B : public A { public: B(int a_, const char* as_, int b_, const char* bs_) : A(a_, as_), // Oberklassen-Subobjekt b(b_), bs(new char[strlen(bs_)+1]) { // lokale Daten strcpy(bs, bs_); } B(const B& rhs) : A(rhs), // Oberklassen-Subobjekt b(rhs.b), bs(new char[strlen(rhs.bs)+1]) { // lokale Daten strcpy(bs, rhs.bs); } ~B() { delete [] bs; } virtual B& assign(const A& rhs) { cout << "virtual B& B::assign=(const A&)" << endl; #ifdef TYPPRUEFUNG if(typeid(*this) != typeid(rhs)) { throw std::bad_typeid(); } #endif B temp(static_cast<const B&>(rhs)); swap(temp); return *this; } B& operator=(const B& rhs) { cout << "B& operator=(const B& rhs)" << endl; return assign(rhs); } virtual void ausgabe() const { A::ausgabe(); cout << "B.b = " << b << ", B.bs = " << bs << " "; } void swap(B& rhs) { A::swap(rhs); // Oberklassendaten std::swap(b, rhs.b); // lokale Daten std::swap(bs, rhs.bs); // lokale Daten } bool operator==(const A& arg) const { const B& rarg = static_cast<const B&>(arg); return A::operator==(arg) && b == rarg.b && strcmp(bs, rarg.bs) == 0; } private: int b; char* bs; }; class C : public B { public: C(int a_, const char* as_, int b_, const char* bs_, int c_, const char* cs_) : B(a_, as_, b_, bs_), // Oberklassen-Subobjekt c(c_), cs(new char[strlen(cs_)+1]) { // lokale Daten strcpy(cs, cs_); } C(const C& rhs) : B(rhs), c(rhs.c), cs(new char[strlen(rhs.cs)+1]) { strcpy(cs, rhs.cs); } ~C() { delete [] cs; } virtual C& assign(const A& rhs) { cout << "virtual C& C::assign=(const A&)" << endl; #ifdef TYPPRUEFUNG if(typeid(*this) != typeid(rhs)) { throw std::bad_typeid(); } #endif C temp(static_cast<const C&>(rhs)); swap(temp); return *this; } C& operator=(const C& rhs) { cout << "C& operator=(const C& rhs)" << endl; return assign(rhs); } virtual void ausgabe() const { B::ausgabe(); cout << "C.c = " << c << ", C.cs = " << cs << " "; } void swap(C& rhs) { B::swap(rhs); // Oberklassendaten std::swap(c, rhs.c); // lokale Daten std::swap(cs, rhs.cs); // lokale Daten } bool operator==(const A& arg) const { const C& rarg = static_cast<const C&>(arg); return B::operator==(arg) && c == rarg.c && strcmp(cs, rarg.cs) == 0; } private: int c; char* cs; }; int main() { // Klasse A cout << "\nTest 1" << endl; A a1(1, "einsA"); A a2(2, "zweiA"); a1.ausgabe(); cout << endl; a1 = a2; cout << "a1 nach Zuweisung a1=a2:\n"; a1.ausgabe(); cout << endl; assert(a1 == a2); // Klasse B cout << "\nTest 2" << endl; B b1(1, "einsA", 2, "einsB"); B b2(3, "zweiA", 4, "zweiB"); b1.ausgabe(); cout << endl; b1 = b2; cout << "b1 nach Zuweisung b1=b2:\n"; b1.ausgabe(); cout << endl; assert(b1 == b2); cout << "\nTest 3 polymorphe Zuweisung"<< endl; B b3(5, "dreiA", 6, "dreiB"); A& ar = b1; // Oberklassenreferenz ar = b3; cout << "ar nach Zuweisung ar=b3:\n"; ar.ausgabe(); cout << endl; assert(ar == b3); // Klasse C cout << "\nTest 4" << endl; C c1(1, "einsA", 2, "einsB", 3, "einsC"); C c2(4, "zweiA", 5, "zweiB", 6, "zweiC"); c1.ausgabe(); cout << endl; c1 = c2; cout << "c1 nach Zuweisung c1=c2:\n"; c1.ausgabe(); cout << endl; assert(c1 == c2); cout << "\nTest 5 : polymorphe Zuweisung A& = C"<< endl; C c3(7, "dreiA", 8, "dreiB", 9, "dreiC"); A& arc = c1; // Oberklassenreferenz arc = c3; cout << "arc nach Zuweisung arc=c3:\n"; arc.ausgabe(); cout << endl; assert(arc == c3); cout << "\nTest 6 : polymorphe Zuweisung B& = C"<< endl; B& brc(c2); // Oberklassenreferenz brc = c3; cout << "brc nach Zuweisung brc=c3:\n"; brc.ausgabe(); cout << endl; assert(brc == c3); // Objket-Sclicing? cout << "\nTest 7 : falscher Typ b1 = c2"<< endl; try { b1 = c3; assert(b1 == c3); // ok (ohne Typprüfung) B::op==() } catch(const std::bad_typeid& e) { cerr << "Falscher Typ! Exception: " << e.what() << endl; } cout << "\nTest 8 : falscher Typ a1 = c2"<< endl; try { a1 = c3; assert(a1 == c3); // ok (nur ohne Typprüfung) A::op==() //assert(c3 == a1); // fail C::op==() } catch(const std::bad_typeid& e) { cerr << "Falscher Typ! Exception: " << e.what() << endl; } cout << "Test-Ende" << endl; }
-
Vielen Dank für die Ergänzungen!
UBr schrieb:
Der Hinweis auf bessere Lösungen in Alexandrescus "Modern C++ Design", ist insofern nicht hilfreich, weil in der dort beschriebenen Clone-Factory Zeiger zurückgegeben werden - das ist eine andere Problemstellung (natürlich trotzdem interessant).
Den Hinweis habe ich im Bezug auf Double-Dispatch gegeben (da bei DWebs Code eine manuelle Typunterscheidung vorkommt). War in der Tat etwas missverständlich.
UBr schrieb:
Verbesserungen und Vereinfachungen?
Mir fallen nicht wirklich Anwendungsbeispiele ein, in denen ein polymorph aufgerufener Zuweisungsoperator im konventionellen Sinne bedeutungsvoll wäre. Damit dieser erfolgreich sein kann, müssen die dynamischen Typen übereinstimmen. Ich habe das Gefühl, dies trifft meist zu, wenn der Aufrufer die Typen kennt und geradeso gut statisch zuweisen könnte. Try&Error (mittels Exception im Fehlerfall) überzeugt mich nicht, womöglich fehlt mir aber nur ein konkretes Szenario.
Daher stelle ich eine etwas andere Vorgehensweise vor, bei der eine Instanz die Zuweisung übernimmt, welche die volle Kontrolle hat und gegebenenfalls den Typ "anpassen" kann, falls Quelle und Ziel nicht übereinstimmen. Die Lösung hängt davon ab, wie Fälle inkompatibler Typen zu behandeln sind. Sowas kann als Fehler gelten, muss aber nicht (z.B. beim Strategy-Pattern, wenn eine neue Strategie von einer bestehenden kopiert und einer anderen zugewiesen wird; hier sind die dynamischen Typen unterschiedlich by Design).
Dazu habe ich an eine spezielle Art Smart-Pointer gedacht, welche das gespeicherte Objekt im Kopierkonstruktor ebenfalls kopiert. Naheliegend wäre eine virtuelle
Clone()
-Funktion. Eine Zuweisung wäre dann mittels Copy-and-Swap implementiert und würde das alte Objekt freigeben sowie ein neues (nicht unbedingt mit demselben Typ) konstruieren.template <typename T> class SmartPtr { public: SmartPtr(const SmartPtr& origin) : myPointer( origin.myPointer->Clone() ) { } SmartPtr& operator= (const SmartPtr& origin) { SmartPtr<T> tmp(origin); Swap(tmp); return *this; } // ... };
Eine allfällige Typprüfung im Zuweisungsoperator wäre mittels
typeid
realisierbar, wobei mir das im Allgemeinen wenig sinnvoll erscheint, zumal es ebenso erlaubt sein sollte, den Zeiger perReset()
oder ähnlich direkt neu zu setzen.Natürlich ist das ein ganz anderer Ansatz als der von dir vorgestellte. Ein Nachteil besteht darin, dass eine Zuweisung eine dynamische Speicheranforderung und -freigabe impliziert, unabhängig davon, ob die Typen direkt zuweisbar wären. Mit gegebenem Small-Object-Allocator könnte die Performance verbessert werden. Andererseits erlaubt die Tatsache, dass der Zuweisungsoperator des Objekts (also
T::operator=
) nie aufgerufen wird, nicht zuweisbare und kopierbare Objekte polymorph zu speichern und dennoch mit Wertsemantik zu versehen, sofernClone()
unterstützt wird.Ein etwas abgeänderter Vorschlag ermöglicht es, sogar Klassen ohne
Clone()
-Funktion polymorph zu kopieren. Dazu muss der dynamische Typ bei der Übergabe an den Smart-Pointer-Konstruktor gleich dem statischen sein, sodass dieser vom Smart-Pointer erkennt wird. Durch Funktionszeiger wird der Kopierkonstruktor des dynamischen Typs aufgerufen. Für den Destruktoraufruf könnte ein ähnlicher Mechanismus eingebaut werden, wodurch der Destruktor nicht einmal virtuell sein müsste. Im folgenden Beispiel istU
entwederT
oder eine vonT
abgeleitete Klasse.template <typename T, typename U> T* Copy(const T* origin) { return new U( *static_cast<const U*>(origin) ); } template <typename T> class SmartPtr { public: template <typename U> SmartPtr(U* pointer) : myCopyFunction(&Copy<T, U>) , myPointer(pointer) { } SmartPtr(const SmartPtr& origin) : myCopyFunction(origin.myCopyFunction) , myPointer( myCopyFunction(origin.myPointer) ) { } // ... private: T* (*myCopyFunction)(const T*); T* myPointer; };
Die Anwendung könnte für beide Implementierungen etwa so aussehen (hier ohne Fehlerbehandlung bei unterschiedlichen Typen):
class Base { ... }; class Derived1 : public Base { ... }; class Derived2 : public Base { ... }; int main() { SmartPtr<Base> a(new Derived1); SmartPtr<Base> b(new Derived2); SmartPtr<Base> c = b; // Kopie; *c hat dynamischen Typ Derived2 b = a; // Zuweisung; *b hat dynamischen Typ Derived1 a = b; // Zuweisung; Typen bleiben gleich (Derived1) }
-
Also ich hab das ganze jetzt mittlerweile endlich verstanden, dank eurer ausführlichen Hilfe
Also vielen Dank nochmal für die Hilfe
Gruß freeG