=-Operator bei Vererbung
-
In folgendem Programm weigert sich der Compiler unten in Zeile 18, den =-Operator aufzurufen. Warum ist das so? Können Operatoren nicht vererbt werden, ohne sie in der abgeleiteten Klasse noch mal explizit neu zu deklarieren? Wenn ich in Zeile 16 nämlich Derived durch Base ersetze, funktioniert es.
class Base { public: virtual int operator = (int value) { return value; } }; class Derived : public Base { }; int main() { Derived d; d = 5; }
-
Problem ist, dass jede Klasse automatisch einen
operator=zur Verfügung stellt. In Derived steht somit einDerived& operator =(Derived const&)bereits zur Verfügung. Dieser überdeckt denoperator =aus Base. Daher gibt es keinen gültigenoperator =. Was du in dem Fall tun musst, ist denoperator =ausBaseraufzuziehen, damit er bei der Auflösung überhaupt in Betracht gezogen wird.class Base { public: virtual int operator = (int value) { return value; } }; class Derived : public Base { public: using Base::operator =; }; int main() { Derived d; d = 5; }Jetzt sollte es gehen.
Grüssli
-
Aber
Derived &operator =(Derived const &)[/cpp] hat ja eine andere Signatur als [c]int operator =(int). Wieso verdeckt der eine dann den anderen? Selbst wenn es diesen implizit vorhandenen Operator in Derived schon gibt, dürfte sich der ja mit dem aus Base kommenden Operator gar nicht in die Quere kommen.
-
Nachtrag:
Was mach ich eigentlich, wenn ich sowas hier habe (siehe Kommentare im Code):class Base { public: virtual int operator =(int value) { return value; } private: Base &operator =(const Base &value); // Keine Implementierung, weil so eine // Zuweisung nicht möglich sein soll. }; class Derived : public Base { public: using Base::operator =; // Das geht jetzt nicht mehr. }; int main() { Derived d; d = 5; }
-
Jimmbob schrieb:
Aber
Derived &operator =(Derived const &)[/cpp] hat ja eine andere Signatur als [c]int operator =(int). Wieso verdeckt der eine dann den anderen? Selbst wenn es diesen implizit vorhandenen Operator in Derived schon gibt, dürfte sich der ja mit dem aus Base kommenden Operator gar nicht in die Quere kommen.Die Signatur spielt absolut keine Rolle. Es geht hier um den Scope, in welchem die Funktion definiert wurde. Es werden zuerst alle
operator=in Derived gesucht. Wenn welche gefunden wurden, dann wird nicht mehr weitergesucht, sondern nur noch der beste ausgewählt. Leider gibt es aber keinen, daher gibt es einen Kompilerfehler. Wenn theoretisch keinoperator=gefunden worden wäre, erst dann würde in Base nachoperator=Methoden gesucht werden. So ist es halt im Standard definiert. Das gilt übrigens ganz allgemein für alle möglichen Funktionen.Zu deinem zweiten Problem:
Die Zuweisung ist an sich schon etwas fraglich. Normalerweise weist man einer Variable nur Werte des gleichen Typs zu. Da kommt doch etwas die Frage auf, ob eine Funktion nicht besser wäre?Grüssli
-
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, was bei polymorphem Code wie
Base* a = ...; Base* b = ...; *a = *b;nur die Basisklassenversion
Base::operator=in Betracht zieht.Alternativen sind oft auch semantisch sinnvoller: Entweder, man verbietet Wertsemantik komplett (durch privaten Kopierkonstruktor und Zuweisungsoperator), oder man lässt sie auf Objekten gleichen Typs (in der gleichen Hierarchiestufe) zu, wodurch man wiederum kein
virtualbenötigt.
-
Die einfachste Möglichkeit einer auch polymorph korrekt funktionierenden Zuweisung ist, in jeder Klasse einer Vererbungshierarchie den operator=() zu implementieren und in ihm eine virtuelle Funktion assign() oder zuweisung() aufzurufen, die die eigentliche Arbeit macht. Beispiel:
Derived& operator=(const Derived& rhs) { return assign(rhs); } virtual Derived& assign(const Base& rhs) { Derived temp(dynamic_cast<const Derived&>(rhs)); swap(temp); return *this; }Der dynamic_cast sorgt für den richtigen Typ. Bei falscher Verwendung (= nicht passende Typen) gibt es eine Exception.
-
Ergänzung: Jede Klasse sollte auch eine Methode swap() haben, die in assign() aufgerufen wird. swap(arg) vertauscht einfach das Argument mit *this.
void swap(Derived& rhs) { X::swap(rhs); // Oberklassendaten std::swap(lokalesDatum1, rhs.lokalesDatum1); // lokale Daten std::swap(lokalesDatum2, rhs.lokalesDatum2); // lokale Daten }X muss dabei die direkte Oberklasse von Derived sein. Falls Derived direkt von Base erbt, gilt also X=Base. Die Methode swap() ruft die entsprechende Funktion der Standardbibliothek auf.
-
UBr schrieb:
Ergänzung: Jede Klasse sollte auch eine Methode swap() haben, die in assign() aufgerufen wird. swap(arg) vertauscht einfach das Argument mit *this.
Warum soll man swap bei einer Zuweisung verwenden?
Das macht ja dein zugewiesenes Objekt 'kaputt'UBr schrieb:
virtual Derived& assign(const Base& rhs) { Derived temp(dynamic_cast<const Derived&>(rhs)); swap(temp); return *this; }Der dynamic_cast sorgt für den richtigen Typ. Bei falscher Verwendung (= nicht passende Typen) gibt es eine Exception.
Ich persönlich würde ein static_cast verwendet, der in den meisten Fällen ausreicht.
EDIT: ups... Falsche Aussage gelöscht
-
CSpille schrieb:
Warum soll man swap bei einer Zuweisung verwenden?
Das macht ja dein zugewiesenes Objekt 'kaputt'Das ganze nennt sich Copy-and-Swap. Dabei wird zuerst eine Kopie des Operanden angelegt und mit der gespwapt. Vorteil: man kann den Kopierkonstruktor wiederverwenden und das ist exceptionsicher.
-
@CSpille:
Base b; Derived d; b = d; // s.u.So eine Zuweisung ist syntaktisch erlaubt, semantisch i.Allg. aber nicht erwünscht. Mit static_cast würde es keine Exception für diesen Fall geben. Nur wenn b=d tatsächlich erlaubt sein soll, ist static_cast besser. Ich kann mir aber keinen praktisch relevanten Anwendungsfall dafür vorstellen.
-
@make it right:
Dessen war ich mir bewusst, jedoch würde ich die Performance bevorzugen.
Im anderen Fall erhältst du ja auch nur einen Runtime-Fehler...Naja...
evlt. dynamic_cast für Debug und static_cast für Release
-
CSpille schrieb:
@make it right:
Dessen war ich mir bewusst, jedoch würde ich die Performance bevorzugen.
Im anderen Fall erhältst du ja auch nur einen Runtime-Fehler...Nicht unbedingt - hängt vom nachfolgenden Code ab. Der Performance-Unterschied ist minimal, weil der Laufzeitaufwand gegenüber dem Kopierkonstruktor (erst recht bei dynamischen Daten) praktisch keine Rolle spielt. Die Performance sollte man da optimieren, wo sie wirklich (=messbar!) aufgefressen wird.
CSpille schrieb:
Naja...
evlt. dynamic_cast für Debug und static_cast für ReleaseWie soll man das pragmatisch und wartungsfreundlich realisieren?
Ist es sinnvoll, dass ein Programm sich möglicherweise bei Debug und Release jeweils verschieden verhält?
-
make it right schrieb:
@CSpille:
Base b; Derived d; b = d; // s.u.So eine Zuweisung ist syntaktisch erlaubt, semantisch i.Allg. aber nicht erwünscht. Mit static_cast würde es keine Exception für diesen Fall geben. Nur wenn b=d tatsächlich erlaubt sein soll, ist static_cast besser. Ich kann mir aber keinen praktisch relevanten Anwendungsfall dafür vorstellen.
Welche Exception?
-
make it right schrieb:
Ist es sinnvoll, dass ein Programm sich möglicherweise bei Debug und Release jeweils verschieden verhält?
Ja.
-
volkard schrieb:
Welche Exception?
std::bad_cast natürlich
-
make it right schrieb:
CSpille schrieb:
@make it right:
Dessen war ich mir bewusst, jedoch würde ich die Performance bevorzugen.
Im anderen Fall erhältst du ja auch nur einen Runtime-Fehler...Nicht unbedingt - hängt vom nachfolgenden Code ab. Der Performance-Unterschied ist minimal, weil der Laufzeitaufwand gegenüber dem Kopierkonstruktor (erst recht bei dynamischen Daten) praktisch keine Rolle spielt. Die Performance sollte man da optimieren, wo sie wirklich (=messbar!) aufgefressen wird.
Also ich persönlich habe einen dynamic_cast (ohne jemals Messungen angestellt zu
haben) immer als sehr teuer angesehen. Immerhin werden (so zumindest meine Vorstellung)
alle möglichen Klassen-IDs verglichen. Somit ist ein dynamic_cast wohl eine
der aufwändigsten Anweisungen.make it right schrieb:
CSpille schrieb:
Naja...
evlt. dynamic_cast für Debug und static_cast für ReleaseWie soll man das pragmatisch und wartungsfreundlich realisieren?
Ist es sinnvoll, dass ein Programm sich möglicherweise bei Debug und Release jeweils verschieden verhält?Evtl. Geschmacksache... Warum schmeißen Collection in Java unter bestimmten
Umständen eine ConcurrentModificationException (auf die man sich nicht verlassen kann).
Ich möchte nur Probleme aufdecken. Ich denke nicht, dass man einen solchen
Mechanismus so einsetzt, dass man diese Exception (oder eben nicht) wirklich möchteOptimal wäre natürlich ein Mechanismus, der zur Kompilierzeit solche Fehler aufdeckt.
Ich persönlich würde wahrscheinlich in meiner Basisklasse den Zuweisungskonstruktor
verbieten und in der Klasse selbst von einem Template ableiten, das den
Zuweisungsoperator implementiert. Falls von dieser Klasse ebenfalls abgeleitet
werden kann, würde ich eine zusätzliche Klasse dazwischen packen, die
alles implementiert außer den Zuweisungsoperator.So hat man dann absolut keine Probleme mehr, weil nur der passende Zuweisungsoperator
deklariert ist.
-
CSpille schrieb:
Also ich persönlich habe einen dynamic_cast (ohne jemals Messungen angestellt zu
haben) immer als sehr teuer angesehen. Immerhin werden (so zumindest meine Vorstellung)
alle möglichen Klassen-IDs verglichen. Somit ist ein dynamic_cast wohl eine
der aufwändigsten Anweisungen.Nicht alle möglichen Klassen-IDs, sondern nur in der Vererbungshierarchie.
D.h. derselbe Aufwand wie bei einem virtuellen Funktionsaufruf.CSpille schrieb:
Optimal wäre natürlich ein Mechanismus, der zur Kompilierzeit solche Fehler aufdeckt.
Stimmt. Natürlich schwierig bei Polymorphismus ...
CSpille schrieb:
Ich persönlich würde wahrscheinlich in meiner Basisklasse den Zuweisungskonstruktor
verbieten und in der Klasse selbst von einem Template ableiten, das den
Zuweisungsoperator implementiert. Falls von dieser Klasse ebenfalls abgeleitet
werden kann, würde ich eine zusätzliche Klasse dazwischen packen, die
alles implementiert außer den Zuweisungsoperator.Klingt komplex. Da wäre ein konkretes Beispiel interessant, um zu sehen, ob das wirklich einfacher ist als die obige Struktur mit assign/swap! Halte ich für nicht möglich, lasse mich aber gern überzeugen.
-
make it right schrieb:
D.h. derselbe Aufwand wie bei einem virtuellen Funktionsaufruf.
Das glaube ich jetzt mal nicht.
-
de scha wü?
-
make it right schrieb:
Klingt komplex. Da wäre ein konkretes Beispiel interessant, um zu sehen, ob das wirklich einfacher ist als die obige Struktur mit assign/swap! Halte ich für nicht möglich, lasse mich aber gern überzeugen.
Ist auch komplex, aber du hast deinen Fehler zur Kompilierzeit
Hier ein Beispiel:
#include<iostream> template<typename T> class Assignable{ public: T& assign(const T& t){ T temp(t); T& tRef = static_cast<T&>(*this); swap(tRef, temp); return tRef; } }; class IA { public: int a; private: IA& operator=(const IA& a); }; class IB : public IA{ public: int b; private: IB& operator=(const IB& a); }; class A :public IA, public Assignable<A>{ public: A& operator=(const A& t){ return assign(t); } }; class B :public IB, public Assignable<B>{ public: B& operator=(const B& t){ return assign(t); } }; void swapInternal(IA& a, IA& b){ std::swap(a.a, b.a); } void swap(A& a, A& b){ swapInternal(a,b); } void swap(B& a, B& b){ swapInternal(a, b); std::swap(a.b, b.b); } int main(){ A a; a.a = 1; B b;; b.a = 1; b.b = 1; A a2; a2 = a; B b2; b2 = b; // b2 = static_cast<IB&>(b); <- Das geht dann nicht!!! return 0; }Die Frage, ob man sich den Aufwand macht, ist natürlich Geschmacksache,
jedoch hat man eine wirklich saubere Schnittstelle...