Copy Konstruktor und Vererbung
-
Als erstes sollte man entscheiden, ob man polymorphes Verhalten zur Laufzeit braucht oder nur während der Übersetzung. Es wurde aber explizit nach einem Verhalten wie in C# gefragt. Da ist clone Laufzeit polymorph. Template Lösungen scheiden damit aus.
hustbaer schrieb:
@~john:
ad 1: richtig, aber die neue Funktion kann ruhig gleich heissen wie die alte. Die alte ist ja immernoch erreichbar (qualifizierter Aufruf), und da sie nicht virtual ist werden Programmteile die nur mit CloneContract* arbeiten auch nie die falsche Funktion aufrufen.
Trotzdem bringt das nichts. Für ein richtige Implementierung von clone braucht man polymorphes Verhalten. Einziges Problem man muß sicherstellen, daß jede Klasse "clone" reimplementiert. Auch ein "clone_impl" schützt nicht davor, daß es zu so einem Problem kommen kann, wenn jemand clone_impl vergißt zu implementieren, das umschliessende clone hilft da nichts.
So meine Version mit der Garantie, daß es kein Slicing via clone gibt. Die Anpassung an std::auto_ptr dürfte wohl trivial sein.
#include <boost/shared_ptr.hpp> #include <stdexcept> #include <string> class Cloneable { protected: template <typename T> static boost::shared_ptr<Cloneable> cloneObject (T const*const self) { if (typeid (*self) != typeid (T)) { std::string s = "Cloneable.clone() slicing "; s += typeid(*self).name(); throw std::logic_error (s); } return boost::shared_ptr<Cloneable> p(new T(*self)); } public: virtual ~Cloneable() = 0; virtual boost::shared_ptr<Cloneable> clone () const = 0; }; Cloneable::~Cloneable() {} class FooB : public Cloneable { public: virtual boost::shared_ptr<Cloneable> clone () const { return Cloneable::cloneObject (this); } }; class FooC : public FooB { public: /* virtual boost::shared_ptr<Cloneable> clone () const { return Cloneable::cloneObject (this); } */ }; int main () { FooC c; boost::shared_ptr<Cloneable> p (c.clone()); }
-
~john schrieb:
Als erstes sollte man entscheiden, ob man polymorphes Verhalten zur Laufzeit braucht oder nur während der Übersetzung.
Das liegt allerdings weitab vom Thema. Im Prinzip wäre es natürlich wünschenswert, die Implementation von clone_impl durch den Compiler aus einem Template, das über den Typ der jeweiligen Klasse parametrisiert ist, generieren zu lassen, so eine Art Template hat C++ allerdings nicht.
~john schrieb:
Es wurde aber explizit nach einem Verhalten wie in C# gefragt.
Es wurde nach einem Verhalten ähnlich dem in C# gefragt. Ich kenne C# nicht, falls das Äquivalent deiner Lösung tatsächlich das Optimum in C# sein sollte, wäre ich etwas enttäuscht.
~john schrieb:
Da ist clone Laufzeit polymorph.
Das ist es bisher in allen Lösungen gewesen.
~john schrieb:
Template Lösungen scheiden damit aus.
Den Schluss kann ich nicht nachvollziehen. Außerdem widersprichst du dir dann selbst mit deiner eigenen Lösung.
hustbaer schrieb:
@~john:
ad 1: richtig, aber die neue Funktion kann ruhig gleich heissen wie die alte. Die alte ist ja immernoch erreichbar (qualifizierter Aufruf), und da sie nicht virtual ist werden Programmteile die nur mit CloneContract* arbeiten auch nie die falsche Funktion aufrufen.
[/quote]Die Möglichkeit des qualifizierten Aufrufes ist gar nicht notwendig, aus Sicht der Basisklasse kommt es überhaupt nicht darauf an, ob ihre Funktionen in einer abgeleiteten Klasse direkt aufrufbar sind, es reicht schon, dass die abgeleitete Klasse wie eine Basisklasse erscheinen kann (per impliziter Pointer-/lvalue-Konvertierung) um LSP zu genügen.
~john schrieb:
Trotzdem bringt das nichts. Für ein richtige Implementierung von clone braucht man polymorphes Verhalten. Einziges Problem man muß sicherstellen, daß jede Klasse "clone" reimplementiert. Auch ein "clone_impl" schützt nicht davor, daß es zu so einem Problem kommen kann, wenn jemand clone_impl vergißt zu implementieren, das umschliessende clone hilft da nichts.
Das ist ziemlich wirr. Wenn der Fehler gemacht wird, eine Funktion nicht zu überschreiben, die überschrieben werden muss, können wir diesen Fehler nicht dadurch beheben, dass wir eine die Implementation einer anderen Funktion ändern (es sei denn, diese Änderung hebt die Bedingung auf, dass die nicht überschriebene Funktion überschrieben werden muss - dann ist jene Funktion allerdings wahrscheinlich völlig überflüssig - und dass ist bei clone offensichtlich nicht der Fall). Die Lösung kann nur darin bestehen, die fehlende Implementation nachzuholen. Punkt.
Im Prinzip kann diese Problem in einem kompilierbaren Programm gar nicht auftreten, denn das bedeutet, dass clone_impl eine Überschreibung in einer nicht abstrakten Basisklasse hat - und die Ableitung von nicht-abstrakten Klassen in einer Hierarchie wird im LSP verletzen (oder trivial und daher überflüssig sein - so wie im Beispiel). Das ist ein schwerer Designfehler, der wiederum nichts mit clone selbst zu tun hat.~john schrieb:
So meine Version mit der Garantie, daß es kein Slicing via clone gibt. Die Anpassung an std::auto_ptr dürfte wohl trivial sein.
#include <boost/shared_ptr.hpp> #include <stdexcept> #include <string> class Cloneable { protected: template <typename T> static boost::shared_ptr<Cloneable> cloneObject (T const*const self) { if (typeid (*self) != typeid (T)) { std::string s = "Cloneable.clone() slicing "; s += typeid(*self).name(); throw std::logic_error (s); } return boost::shared_ptr<Cloneable> p(new T(*self)); } public: virtual ~Cloneable() = 0; virtual boost::shared_ptr<Cloneable> clone () const = 0; }; Cloneable::~Cloneable() {} class FooB : public Cloneable { public: virtual boost::shared_ptr<Cloneable> clone () const { return Cloneable::cloneObject (this); } }; class FooC : public FooB { public: /* virtual boost::shared_ptr<Cloneable> clone () const { return Cloneable::cloneObject (this); } */ }; int main () { FooC c; boost::shared_ptr<Cloneable> p (c.clone()); }auch das ist kein richtiges Clonen. Wenn ich ein A, das Teil eine B-Objektes ist, habe, möchte ich nach dem Clonen ein A, welches Teil einer B-Kopie ist, haben; hier bekomme ich etwas Seltsames, das Teil einer B-Kopie ist. All die Lösungen, die eine zusätzliche Basisklasse einführen, drängen den Klasse unnötige Restriktionen auf, zudem versagen sie schnell in komplizierter Mehrfachvererbung - zwar ist die selten, aber gerade ein sehr einfaches und nützliches Konzept wie Cloning lebt davon, sehr allgemeingültig zu sein.
Die Lösung (die einzige Restriktion ist hier die Beschränkung auf new/delete, eigentlich gehört dort ein move_ptr hin), die mir am Besten gefällt (ja ich weiß, kein echtes C#-Äquivalent für... C#-Puristen?). Ja es ist furchtbar kompliziert zu implementieren, aber in der Anwendung umso einfacher:
template<typename T> typename boost::enable_if< boost::is_polymorphic< T >, std::auto_ptr<T> >::type clone(const T* p) { // jetzt wird es ein bisschen ungemütlich, um bei Mehrfachvererbung, bei der der Upcast mehrdeutig wäre, das richtige Subobjekt wiederzufinden // wir machen uns zunutze, dass das Speicherlayout für alle Objekte des gleichen dynamischen Typs gleich ist // ein direkter cast kommt nicht in Frage, denn der kann mehrdeutig sein const char* const complete_source_object = static_cast< const char* >( dynamic_cast< const void* >( p ) ); const std::ptrdiff_t offset_of_subobject = reinterpret_cast< const char* >( p ) - complete_source_object; void* const complete_clone = p->clone_impl(); void* const subobject_clone = static_cast< char* >( complete_clone ) + offset_of_subobject; return std::auto_ptr<T>( static_cast< T* >( subobject_clone ) ); } // für nicht-polymorphe Typen kopieren wir einfach ganz normal, um einheitliche Behandlung zu ermöglichen template<typename T> typename boost::disable_if< boost:is_polymorphic< T >, std::auto_ptr<T> >::type clone(const T* p) { return std::auto_ptr<T>( new T( *p ) ); } // jede polymorphe Klasse benötigt dann eine entsprechende Funktion clone_impl, auf die clone zugreifen kann, die - wie auch immer - das vollständige Objekt kopiert und eine Zeiger darauf zurückgibt. class Foo { ... friend std::auto_ptr<Foo> clone<Foo>(const Foo* p); protected: virtual void* clone_impl() const = 0; // oder auch Foo* clone }; class Bar : public Foo { ... // jede Klasse benötigt diese Deklaration friend std::auto_ptr<Bar> clone<Bar>(const Bar* p); // in jeder nicht-abstrakten Klasse: private: virtual void* clone_impl() const { return Bar( *this ); } }; // jeder nicht-polymorphe Typ ist sofort Cloneable, sofern auf den Copy-ctor zugegriffen werden kann
-
hustbaer schrieb:
@~john:
ad 2: Was wenn CloneExtendedContract NICHT der konkrete Typ ist, sondern selbst nur eine Basisklasse?
Dann passiert gar nichts, da man von abstrakten Typen keine Objekte erzeugen kann. Man kann in C++ Cloneable pure virtual machen und CloneableExtended ebenfalls. Gefährlich wird es nur bei konkrete Klasse erbt von konkreter Klasse, dann besteht die Gefahr, daß eine der Methoden nicht redefiniert wird. Also, muß man nur darauf achten, daß es in so einem Fall die Methode einer Klasse darüber (oder war's darunter) nicht mißbraucht werden kann. Code habe ich dazu ja gepostet.
ad 3: in dem speziellen Fall sehe ich wirklich kein Problem. Die Funktion macht immer dasselbe, der Returnwert ist semantisch immer gleich, bloss der Typ unterscheidet sich. Kannst du für diesen konkreten Fall ein Beispiel zeigen wo es zu Problemen kommen könnte, oder gar "gefährlich" wäre?
Erstens vernebelt die Member Function die Tatsache, daß die eigentliche Arbeit nur in der virtuellen Methode durchgeführt wird. Es hat also gar keinen Sinn sie zu definieren. Zweitens kann dies beim Ableiten schnell zu Fehlern führen, wenn jemand die Methode wie folgt schreibt
class FooC : public FooB { ... std::auto_ptr<FooB> clone () const { return this->clone_impl(); } // clone_impl wird nicht reimplementiert ... };
-
camper schrieb:
auch das ist kein richtiges Clonen.
Du hast doch nur einen Template Verbau drumherum gemacht, damit es auch ohne Mehrfachvererbung funktioniert, und dadurch wurde es notwendig, die Methode als freistehende Funktion zu implementieren. Java & Co. benutzen den Interface Klassen Ansatz == Mehrfachvererbung, das hatte ich schon zu Anfang geschrieben.
-
camper schrieb:
Im Prinzip kann diese Problem in einem kompilierbaren Programm gar nicht auftreten, denn das bedeutet, dass clone_impl eine Überschreibung in einer nicht abstrakten Basisklasse hat - und die Ableitung von nicht-abstrakten Klassen in einer Hierarchie wird im LSP verletzen (oder trivial und daher überflüssig sein - so wie im Beispiel).
Das ist unsinnig. Man kann sehr wohl von einer konkreten Klasse erben, die schon die pure virtual Methode definiert hat, und dann kommt es möglicherweise zu Slicing. Es gibt in C++ kein Sprachkonstrukt, daß erzwingen würde, daß alle Klassen eine bestimme Methode redefinieren.
P.S. Mein Beispiel zeigt die Problematik übrigens auf.
-
@~john:
~john schrieb:
hustbaer schrieb:
@~john:
ad 2: Was wenn CloneExtendedContract NICHT der konkrete Typ ist, sondern selbst nur eine Basisklasse?
Dann passiert gar nichts, da man von abstrakten Typen keine Objekte erzeugen kann. Man kann in C++ Cloneable pure virtual machen und CloneableExtended ebenfalls. Gefährlich wird es nur bei konkrete Klasse erbt von konkreter Klasse, dann besteht die Gefahr, daß eine der Methoden nicht redefiniert wird. Also, muß man nur darauf achten, daß es in so einem Fall die Methode einer Klasse darüber (oder war's darunter) nicht mißbraucht werden kann. Code habe ich dazu ja gepostet.
Ich meinte genau den Fall dass eine konkrete Klasse von einer anderen konkreten Klasse abgeleitet ist. Du hast in deinem "Punkt 2" angesprochen man könnte ja den copy-ctor verwenden, ich wollte nur darauf hinweisen dass die Verwendung des copy-ctor eben in genau dem Fall zu slicing führen kann.
Weiters wundert es mich etwas dass du den Punkt anführst jmd. könnte vergessen die clone oder clone_impl Funktion zu überschreiben, da du mir ja einmal bei ähnlichen Bedenken vorgehalten hast das sei bloss ein "social engineering" Problem. Liegt hier auch nicht anders. (Davon abgesehen dass der Begriff IMO in beiden Fällen falsch angewendet ist)
Deine "slicing freie" Version funktioniert im Übrigen auch nur bis dorthin wo jmd. die "clone" Methode falsch implementiert (nämlich ohne das cloneObject Template zu verwenden). Wenn man davon dann eine weitere Klasse ableitet die clone nicht selbst implementiert krachts wieder.
-
Guten Morgen,
danke für die vielen Ideen...für die, die nachgefragt haben, wie das in C# funktioniert,
Object.MemberwiseClone() erstellt ein neues leeres Objekt von dem Objekt, dass übergeben wurde. (In C# kann man den Klassentyp mit obj.GetType() zur Laufzeit ermitteln und mit Activator.CreateInstanz einen Konstruktor aufrufen, egal ob sichtbar oder nicht). Anschließend werden alle Felder des Ursprungsobjekt auf das neue Objekt kopiert. (Ohne das, das in einem Copy-Konstruktor oder einer Clone-ähnlichen-Methode realisiert werden muss.)
-
hustbaer schrieb:
@~john:
Weiters wundert es mich etwas dass du den Punkt anführst jmd. könnte vergessen die clone oder clone_impl Funktion zu überschreiben, da du mir ja einmal bei ähnlichen Bedenken vorgehalten hast das sei bloss ein "social engineering" Problem. Liegt hier auch nicht anders. (Davon abgesehen dass der Begriff IMO in beiden Fällen falsch angewendet ist)Ja, in beiden Fällen ist das der Fall, und ja ich sehe das als ein social enginering Problem, denn es gibt Kollegen die nicht so sonderlich achtsam sind, um das Problem zurückhaltend zu umschreiben.
Deine "slicing freie" Version funktioniert im Übrigen auch nur bis dorthin wo jmd. die "clone" Methode falsch implementiert (nämlich ohne das cloneObject Template zu verwenden). Wenn man davon dann eine weitere Klasse ableitet die clone nicht selbst implementiert krachts wieder.
Stimmt, aber Cut&Paste ist eine sehr beliebte "Programmiertechnik", somit hat man das Risiko minimiert. In der Ursprungsfassung hatte ich dies nicht eingebaut, so minimiert man die Eigenleistung für diese Methode nur mit einer Template Klasse kann man das Problem wirklich umschiffen, aber kommen andere Dinge ins Spiel.
-
hustbaer schrieb:
@~john:
Ich meinte genau den Fall dass eine konkrete Klasse von einer anderen konkreten Klasse abgeleitet ist. Du hast in deinem "Punkt 2" angesprochen man könnte ja den copy-ctor verwenden, ich wollte nur darauf hinweisen dass die Verwendung des copy-ctor eben in genau dem Fall zu slicing führen kann.
Was meinst Du konkret damit?
Oder ist es der von mir skizzierte Fall gemeint?
-
~john schrieb:
hustbaer schrieb:
@~john:
Ich meinte genau den Fall dass eine konkrete Klasse von einer anderen konkreten Klasse abgeleitet ist. Du hast in deinem "Punkt 2" angesprochen man könnte ja den copy-ctor verwenden, ich wollte nur darauf hinweisen dass die Verwendung des copy-ctor eben in genau dem Fall zu slicing führen kann.
Was meinst Du konkret damit?
Oder ist es der von mir skizzierte Fall gemeint?Ja, der von dir skizzierte Fall war das was ich immer schon meinte. Dort kann es eben wie erwähnt zu slicing kommen, wenn man den copy-ctor verwendet.
-
OT: Das ist typisch C++ Programmierer. Ueber sowas triviales wie das Clonen von Objekten koennen sie 10 Seiten fuellen.

-
OT: Fresse, DEvent!

-
DEvent schrieb:
OT: Das ist typisch C++ Programmierer. Ueber sowas triviales wie das Clonen von Objekten koennen sie 10 Seiten fuellen.

... sprach der Ingenieur zum Mathematiker und teilte durch 0.

Gruß,
Simon2.