Seite 1 von 1
Vererbung des Todes.
Verfasst: 12.09.2016, 15:45
von DerAlbi
Hallo Leute,
ich habe ein strukturelles Problem:
Code: Alles auswählen
struct IBaseInterface
{
static constexpr int Magic = 123;
virtual ~IBaseInterface() {}
int Implement() { return Magic; };
};
struct IOtherBaseInterface
{
virtual ~IOtherBaseInterface() {}
virtual int Implement() = 0;
};
struct CDerived: public IBaseInterface, public IOtherBaseInterface
{
virtual ~IDerived() {}
virtual int Implement() { return IBaseInterface::Implement(); }
};
int main()
{
CDerived d;
IOtherBaseInterface* i = &d;
volatile int a = d.Implement();
volatile int b = i->Implement();
(void)a;
(void)b;
}
CDerived muss aufgrund der pure virtual Vererbung von
IOtherBaseInterface die Methode
int Implement() implementieren.
Diese ist bereits in der anderen parallelen Basisklasse
IBaseInterfaceimplementiert.
Kann ich diese bekloppte Indirektion in
CDerivedirgendwie wegbekommen, sodass bei
d.Implement() sofort
IBaseInterface::Implement() aufruft (wobei das der Optimizer vermutlich rafft) und auch bei
IOtherBaseInterface->Implement sofort der Call auf
IBaseInterface::Implement() umgeleitet wird und nicht erst über
CDerived::Implement() geht?
Ich hab so mega das Bedürfnis es mit using zu lösen... aber äähm. Albi dumm. Das muss sich doch über die vtable umbiegen lassen :-/
Re: Vererbung des Todes.
Verfasst: 12.09.2016, 16:18
von dot
Ich vermute mal, dass virtuelle Vererbung ist, was du suchst; aber fangen wir mal beim Grundproblem an: Wieso erbt das Ding von zwei verschiedenen Interfaces, die beide eine unterschiedliche Methode mit dem selben Namen haben?
Re: Vererbung des Todes.
Verfasst: 12.09.2016, 22:37
von DerAlbi
Mit virtual vor den Vererbungen habe ich auch schon rumprobiert - leider kein erfolg. Ich mache da aber auch nur irgendwas nicht deterministisches, weil ich auch nicht weiß, wie die endlösung auszusehen hat :-/
Zur Struktur: Das ganze ist ein Hardware-Abstraction-Layer für einen Mikrocontroller.. In der Takterzeugung habe ich nun den Fall, dass ich mehere Interfaces zusammen bringe, und da kommen leider solche Sturkturen raus-
Ich habe Abstraktionsklassen die aus verschiedenen Interfaces bestehen. Damit kann man einen Oszillator z.B. als IBaseOscillator betrachten, oder aber auch als ISysClkSrc, falls man den Oszillator direkt als Haupttaktquelle verwenden kann. ISysClkSrc muss z.B. in dem Fall eine Funktion IsReady() anbieten, damit ich aus sicht der SystemTaktquelle weiß, ob der Oszillator läuft oder nicht. IsReady() ist aber in IBaseOscillator implementiert (bzw in einer Konkretisierung davon).
Damit kann ich die ganzen Oszillatoren im Mikrocontroller Instantiieren und per Typsicherheit garantieren, welcher Oszillator bei der HardwareInitialisierung wo nutzbar ist.
Gleiches gilt beim Aufsetzen der PLL: nicht jeder IBaseOszillator ist ein möglicher PLL-Input, aber jeder PLL-Input in ein IBaseOszillator. Also vererbe ich bei jedem DerivedOscillator von IPLLSrcClk. Aber auch als IPLLSrcClk muss man wissen, ob der darunterliegende Oszillator IsReady() ist usw.
Zur Zeit habe ich das so gelöst, dass jeder IPLLSrcClk im Konstruktor einen IBaseOszillator mitbekommt, um auf die Funktionen von IBaseOszillator zugriff zu bekommen. Das finde ich aber grundlegend falsch.
Der obige Ansatz soll das umgehen, aber da ist die Implementierung ja höchst unzufriedenstellend.
Kurzum: Komplizierte Hardware, komplizierte Software. Leider hat das seine Berechtigung. Ich glaube auch nicht, dass aus meiner Beschreibung das jetzt wirklich klar wird, warum ich das so mache. Da müsst ich den ganzen Code zeigen, aber auch das wäre nicht hilfreich & zielführend, ohne dass andere in den speziellen Mikrocontroller eingearbeitet sind.
Glücklicher weiße kann man es im obigen Testfall auf ein konkretes aber allgemeines Problem reduzieren. :-)
Re: Vererbung des Todes.
Verfasst: 12.09.2016, 22:59
von dot
Ok, du willst also genau was ich dachte: Du willst ein Interface und dann willst du partielle Implementierungen davon in eine konkrete Klasse erben. Meine Lösung für Interfaces in C++ sieht wie folgt aus (Methoden sind pure virtual und die einzigen public Member, die großen 3/5 sind protected; das verhindert Slicing, Klassen, die das Interface implementieren, können aber trozdem noch Value/Move Semantics haben; erben von Interfaces immer virtual, nur so bekommst du das für Interfaces erwartete Verhalten, dass eine partielle Defaultimplementierungen von Basisklassen bereitgestellt werden können):
Code: Alles auswählen
#ifdef _MSC_VER
# define INTERFACE __declspec(novtable)
#else
# define INTERFACE
#endif
class INTERFACE IBlaBlub
{
protected:
IBlaBlub() = default;
IBlaBlub(IBlaBlub&&) = default;
IBlaBlub(const IBlaBlub&) = default;
IBlaBlub& operator =(IBlaBlub&&) = default;
IBlaBlub& operator =(const IBlaBlub&) = default;
~IBlaBlub() = default;
public:
virtual int blub() = 0;
virtual int blab() = 0;
};
class INTERFACE BasicBlab : public virtual IBlaBlub
{
protected:
BasicBlab() = default;
BasicBlab(BasicBlab&&) = default;
BasicBlab(const BasicBlab&) = default;
BasicBlab& operator =(BasicBlab&&) = default;
BasicBlab& operator =(const BasicBlab&) = default;
~BasicBlab() = default;
public:
int blab() override { return 23; }
};
class MyThing : public virtual IBlaBlub, BasicBlab
{
int blub() override { return 42; }
};
int main()
{
MyThing thing;
IBlaBlub* I = &thing;
int a = I->blub();
int b = I->blab();
return 0;
}
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 03:24
von DerAlbi
Damit machst du aber jedes BasicBlab kompatibel zu IBlaBlub Das ändert die Hierarchie doch gewaltig. In der Tat kann man von MyThing gar nicht auf BasicBlab kommen.
In meinem Fall ist das schon noch ein Stück anders. Da ist die Hierarchie eher parallel und nicht von oben nach unten. Ich muss dem Compiler quasi beibringen zur Seite zu gucken.
Kannst du mal konkret mein Code-Beispiel so modifizieren, wie du das meinst?
Ich brauch zum Schluss ein Kind-Objekt das aus gleichzeitig aus A und B besteht wo sich A und B teilweise Überlappen. Die Überlappung ist aber nur entweder in A oder in B implementiert. Dennoch möchte ich, wenn ich auf auf das Kind als A* gucke, die überlappenden Methoden, die in B implementiert sind aufrufen können. Ohne Umwege.
Kacke man. Das kann man nichtmal so beschreiben, dass es Sinn ergibt -.- Sry.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 10:19
von Chromanoid
Solltest Du nicht am besten alle "Funktions-Bündel", die das gleiche bedeuten, in eigene Interfaces packen? Also folgende Klassen: IReadyCheckable, IBaseOszillator (erbt von IReadyCheckable) und IPLLSrcClk (erbt von IReadyCheckable).
Vielleicht wäre es auch besser mit Delegation zu arbeiten und je nach Möglichkeiten z.B. eine Source für irgendwas nur als "SourceFuerIrgendwasProvider" (mit asSourceFuerIrgendwas()) zu exponieren. Dann kann die eigentliche Implementierung der Source leichter ausgetauscht werden und es ist explizit modelliert, dass Komponente X als Quelle für Y benutzt werden kann.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 10:50
von dot
DerAlbi hat geschrieben:Damit machst du aber jedes BasicBlab kompatibel zu IBlaBlub Das ändert die Hierarchie doch gewaltig. In der Tat kann man von MyThing gar nicht auf BasicBlab kommen.
Was genau ist das Problem daran, dass
BasicBlab "kompatibel" zu
IBlaBlub ist. Wieso haben IBaseInterface und IOtherBaseInterface in deinem Beispiel dann zwei verschiedene Methoden mit dem selben Namen, wieso ist eine davon pure Virtual!?
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 11:05
von DerAlbi
Ich rast aus. Das geeeeihht!
Code: Alles auswählen
struct INeedsImplement
{
virtual ~INeedsImplement() {}
virtual int Implement() = 0;
};
struct IBaseInterface: virtual public INeedsImplement
{
static constexpr int Magic = 123;
virtual ~IBaseInterface() {}
virtual int Implement() override { return Magic; };
};
struct IOtherBaseInterface: virtual public INeedsImplement
{
virtual ~IOtherBaseInterface() {}
};
struct IDerived: public IBaseInterface, IOtherBaseInterface
{
virtual ~IDerived() {}
//virtual int Implement() { return IBaseInterface::Implement(); }
};
int main()
{
IDerived d;
IOtherBaseInterface* i = &d;
volatile int a = d.Implement();
volatile int b = i->Implement();
(void)a;
(void)b;
}
Ich gucke mal, dass ich das so implementieren kann. Wird bisschen arbeit. Ich glaub da muss ich ne Menge umstellen.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 12:59
von DerAlbi
Doppelpost, weil ich vorhin Dot noch nicht gesehen hatte, bevor ich im Antwort-Formular war.
Was genau ist das Problem daran, dass BasicBlab "kompatibel" zu IBlaBlub ist. Wieso haben IBaseInterface und IOtherBaseInterface in deinem Beispiel dann zwei verschiedene Methoden mit dem selben Namen, wieso ist eine davon pure Virtual!?
Weil ich sicherstellen muss, dass die Methode im Kind implementiert ist. Also sie muss dort als implementiert erscheinen. Implementiert worden sein kann sie natürlich von jemanden parallel in der Hierarchie.
Warum die "kompatibilität" schlecht ist: Weil man parallel vererben will und nicht abwärts einer Hierarchie. Ich will eine Kindklasse zu dem
einen oder
anderen machen können, aber nicht das
eine zum
anderen.
Ich bin mitlerweile mit der Code-Überarbeitung fertig. fand ich ganz schön kompliziert, so viel Code umzustellen. Man wird von Fehlermeldungen kompeltt überrannt. Aber es hat sich übel gelohnt. Und das Beste: es hat in der Tat inkosistenzen in meinem Interface aufgezeigt. Und einige Interface-Namen muss ich jetzt auch nochmal umbenennen, weil klarer wird, was sie eigentlich implementieren.
Ich freu mich mega. Viiiiel besser.
Das mit der virtuellen Vererbung muss ich jetzt aber erstmal noch nachlesen. So 100% klar ist mir nicht, warum das nun alles funktioniert ^_^
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 14:22
von Helmut
Also ich habe ja noch nie so ganz die Daseinsberechtigung für virtuelle Vererbung gesehen. Da ist es doch schön zu sehen, dass es tatsächlich einen Verwendungszweck gibt :)
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 14:58
von Chromanoid
So ähnlich funktionieren ja auch Interfaces in Java. Mit den default-Implementierungen, die in Java 8 dazu gekommen sind, ist die Usability wahrscheinlich sogar ziemlich ähnlich. Bei Java wird man dann vom Compiler informiert, wenn es nicht eindeutig auflösbare Kollisionen der Default-Implementierungen gibt und man muss sich explizit entscheiden (MyInterface.super.method()). Ansonsten wird in Java immer die spezifischste Implementierung gewählt. Ist das in C++ von der Reihenfolge der Vererbung abhängig?
Was passiert, wenn Du statt
struct IDerived: public IBaseInterface, IOtherBaseInterface
struct IDerived: public IOtherBaseInterface, IBaseInterface
schreibst?
Ich habe vor Urzeiten sehr naiv in C++ Mehrfachvererbung eingesetzt, um mehr oder weniger komponentenorientiert zu arbeiten. Irgendwann habe ich dann gesehen, was da mit dem Speicherlayout abging - hat mich ziemlich frustriert... Dass man virtuelle Vererbung nutzen kann, wusste ich damals leider nicht.. Dieser Artikel scheint das ganz gut zu erklären, finde ich:
http://www.drdobbs.com/cpp/multiple-inh ... /184402074
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 18:07
von DerAlbi
Also ich habe jetzt keine Abhängigkeiten von der Vererbungsreihenfolge gesehen.
Prinzipiell muss man in der Initialiter-List des Konstruktors die Reihenfolge hinterherziehen, damit keine Warnung [-Wreorder] geworfen wird.
Ansonsten kann es mMn auch keine Abhängikeit von der Reihenfolge geben, weil "ein gesundes Interface" die Methoden nur 1x implementiert und duch die Parallelvererbung keine doppelten Implementationen gibt, obwohl es sehr wohl doppelte deklarationen gibt. Durch die virtual-Vererbung werden doppelte Deklarationen aber aufeinander gelegt, so wie ich das verstanden habe.
Also egal wie oft eine virtual-Basis im Interface vorkommt... es gibt davon exakt 1 Instanz.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 18:22
von Chromanoid
Was würde denn IDerived ausgeben, 123 oder 0? edit: lol, ich habe die Syntax für pure virtual vercheckt... Was wäre wenn beide IBaseInterface und IOtherBaseInterface Implement überschreiben und dann in einer Klasse zusammenkommen? Also das klassische Diamond-Pattern meine ich. Da würde dann je nach Aufrufer entweder die Methode aus IBaseInterface oder aus IOtherBaseInterface aufgerufen weden, oder? Das hört sich für mich nach einem ziemlichen Alptraum an...
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 18:52
von dot
DerAlbi hat geschrieben:Doppelpost, weil ich vorhin Dot noch nicht gesehen hatte, bevor ich im Antwort-Formular war.
Was genau ist das Problem daran, dass BasicBlab "kompatibel" zu IBlaBlub ist. Wieso haben IBaseInterface und IOtherBaseInterface in deinem Beispiel dann zwei verschiedene Methoden mit dem selben Namen, wieso ist eine davon pure Virtual!?
Weil ich sicherstellen muss, dass die Methode im Kind implementiert ist. Also sie muss dort als implementiert erscheinen. Implementiert worden sein kann sie natürlich von jemanden parallel in der Hierarchie.
Ich nehme an, dass dir mittlerweile eh klar ist, dass das genau das ist, was die vorgeschlagene Lösung, Interfaces über virtuelle Basisklassen zu repräsentieren, tut... ;)
DerAlbi hat geschrieben:Das mit der virtuellen Vererbung muss ich jetzt aber erstmal noch nachlesen. So 100% klar ist mir nicht, warum das nun alles funktioniert ^_^
Kurzfassung:
In deinem ursprünglichen Ansatz hattest du zwei verschiedene Klassen, die beide eine Methode mit dem selben Namen hatten. Eine "normale" Basisklasse wird einfach zu einem Subobjekt der abgeleiteten Klasse und fertig. Für dein
CDerived bedeutete das, dass es ein
IBaseInterface Subobjekt und ein
IOtherBaseInterface Subobjekt hatte, beide völlig unabhängige Typen die zufällig jeweils eine Methode namens
Implement haben. Dein
CDerived erbt beide
Implement Methoden, du kannst sie über
CDerived aber nie aufrufen da dein
CDerived eben zwei verschiedene Methoden – eine für das erste Subobjekt und eine für das zweite – mit gleichem Namen hat und der Name-Lookup daher mehrdeutig ist. Was du machen kannst ist dein
CDerived auf die jeweilige Basisklasse zu casten und so explizit erst das Subobjekt auszuwählen und dann direkt die entsprechende
Implement Methode darauf aufzurufen, einen qualifizierten Methodennamen anzugeben (z.B.
d.IBaseInterface::Implement()), oder per using-Deklaration explizit einen der beiden Names in
CDerived einzufüren. Sobald dein
CDerived selbst eine Methode namens
Implement deklariert, shadowed dieser Name die gleichnamigen Methoden der beiden Basisklassen und der Name-Lookup ist wieder eindeutig. Alles was du damit erreichst ist aber, dass eindeutig bestimmt ist, welche Methode von welchem Subobjekt nun aufgerufen wird. Keine dieser Vorgehensweisen löst das eigentliche Problem, welches da ist, dass es sich bei deinen Klassen und Methoden um völlig unabhängige Klassen und Methoden handelt, die einfach nur nebeneinander in ein Objekt gesteckt wurden ohne dabei irgendeinen Zusammenhang zu modellieren.
Was du eigentlich willst, ist, dass es ein Interface mit Methoden und Base-Classes mit Standardimplementierungen einiger dieser Methoden gibt. Eine konkrete Klasse soll das Interface implementieren und gleichzeitig Standardimplementierungen hereinerben, teilweise weiter überschreiben sowie noch nicht implementierte Interfacemethoden definieren können. Damit das geht müssen deine Standardimplementierungsklassen aber die jeweiligen Methoden des ursprünglichen Interface überschreiben und nicht einfach nur zusammenhangslos neue Methoden mit dem selben Namen hinzufügen. Damit eine Standardimplementierungsklasse Methoden des Interface überschreiben kann, muss sie selbst vom Interface ableiten. Wenn du einfach nur zwei unverwandte Basisklassen mit gleichnamigen Methoden hast, dann gibt es zwischen diesen Methoden keinerlei Beziehung. Das sind dann einfach zwei verschiedene Methoden die nichts miteinander zu tun haben; sie liegen in separaten, nicht verschachtelten Namensräumen, so etwas wie das Konzept von Überschreibung macht zwischen solchen Strukturen nichtmal Sinn.
Dabei kommt es nun zu folgendem Problem: Deine konkrete Klasse soll das Interface implementieren, muss also vom Interface erben. Da sie vom Interface erbt, hat sie ein Subobjekt vom entsprechenden Typ. Deine Standardimplementierungsklassen müssen vom Interface erben. Da sie vom Interface erben, haben sie jeweils ein eigenes Subobjekt vom entsprechenden Typ. Deine konkrete Klasse soll zusätzlich zum Interface auch von einigen Standardimplementierungsklassen erben, sie hat daher zusätzlich ein Subobjekt für jede Standardimplementierung. Und jedes dieser Subobjekte hat nochmals ein eigenes Interface-Subobjekt. Das allein hilft uns also leider noch nicht; unsere Namensräume haben nun zwar eine passende Hierarchie, aber da unsere konkrete Klasse sowie jede Standardimplementierungsklasse ihr eigenes Interface-Subobjekt mitbringt haben wir immer noch separate Überschreibungsketten der Interface Methoden für jedes Interface-Subobjekt (anders ausgedrückt: Für jedes Interface-Subobjekt existiert ein separater Satz der Interface Methoden). Was wir eigentlich wollen ist, dass sowohl unsere konkrete Klasse als auch alle Standardimplementierungsklassen sich ein Interface-Subobjekt teilen. Dann gibt es für jede Methode dieses gemeinsamen Interface-Subobjektes auch nur eine gemeinsame Überschreibungskette. Und genau das macht die virtuelle Vererbung. Eine virtuelle Basisklasse wird nicht einfach direkt zu einem Subobjekt der abgeleiteten Klasse. Die abgeleitete Klasse enthält stattdessen einen Pointer auf das zu verwendende Basisklassenobjekt. Für jede virtuelle Basisklasse in einer Vererbungshierarchie wird nur ein Subobjekt im most-derived Object angelegt das von allen geteilt wird, indem deren virtual Base-Pointer auf die Adresse des gemeinsamen Basisklassenobjektes gesetzt werden.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 20:09
von DerAlbi
:-D das war aber eine Langfassung :-) Die Kurzfassung wäre die letzten 3 Sätze ;-)
Chromanoid hat geschrieben:Das hört sich für mich nach einem ziemlichen Alptraum an...
Guck nochmal auf den Thread-Namen.
Wobei ich meine Klassen-Hierarchie mitlerweile echt sehr mag. Ich bin stolz drauf :-) Aber eine Situation, wie du sie beschreibst, habe ich gar nicht.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 20:46
von Krishty
Chromanoid hat geschrieben:Was würde denn IDerived ausgeben, 123 oder 0? edit: lol, ich habe die Syntax für pure virtual vercheckt... Was wäre wenn beide IBaseInterface und IOtherBaseInterface Implement überschreiben und dann in einer Klasse zusammenkommen? Also das klassische Diamond-Pattern meine ich. Da würde dann je nach Aufrufer entweder die Methode aus IBaseInterface oder aus IOtherBaseInterface aufgerufen weden, oder? Das hört sich für mich nach einem ziemlichen Alptraum an...
Nein. Es existiert nur ein Methodenzeiger, der überschrieben werden kann. Beim Implementieren der Methode muss man sich für eine Basisklasse entscheiden, und die ist für alle Aufrufer gleich. Das Ganze ist deutlich einfacher verständlich, wenn man sich das Objekt-Layout (vftable-Zeiger und geerbte Attribute) vor Augen führt.
Was mir Gelegenheit für *meine* Meinung gibt: Mach’s doch mit Funktionszeigern. Die kannst du direkt belegen wie du willst, und
überClock.isReady = &ThatOneClock::isReady; verstehe ich schneller als virtuelle Vererbung von zwei Basisklassen, die beide die Methode in der Schnittstelle tragen, aber nur eine hat sie definiert.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 21:54
von Chromanoid
Beim Implementieren der Methode muss man sich für eine Basisklasse entscheiden
D.h. der Compiler meckert oder wo muss man sich für eine Implementierung entscheiden? Ich hatte die Ausführungen bei drdobbs so verstanden, dass je nach dem wie das Objekt gerade gecastet ist, mal die eine mal die andere Implementierung verwendet wird - also wenn man die Methode als unschuldiges Klässchen von zwei Super-Klassen bekommt und sie nicht selbst implementiert.
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 22:04
von dot
Chromanoid hat geschrieben:Was wäre wenn beide IBaseInterface und IOtherBaseInterface Implement überschreiben und dann in einer Klasse zusammenkommen? Also das klassische Diamond-Pattern meine ich. Da würde dann je nach Aufrufer entweder die Methode aus IBaseInterface oder aus IOtherBaseInterface aufgerufen weden, oder? Das hört sich für mich nach einem ziemlichen Alptraum an...
Nun, C++ erlaubt dir, so etwas zu bauen, die Frage ist, wieso du so etwas würdest bauen wollen. Aber wenn du wirklich meinst, dass du sowas unbedingt haben musst, kannst du es bauen und wenn du es baust, dann wird C++ sich auf wohldefinierte und sinnvolle Art und Weise und insbesondere
in sich konsistent verhalten ("sinnvolles" Verhalten derart komplexer Gebilde ist notwendigerweise halt dementsprechend komplex; aber du hast dir dein Gebilde dann halt auch selbst so geschaffen). Generell ist der ganze Vererbungsmechanismus in C++ wesentlich weiter durchdacht als in den meisten anderen Sprachen. Mein Lieblingsbeispiel: Aufruf von virtuellen Methoden aus einem Basisklassenkonstruktor. Wenn ich beispielsweise in Java oder C# in einem Basisklassenkonstruktor eine virtuelle Methode aufrufe, dann führt dieser Aufruf mich immer in den final Overrider des most-derived Type. Vielleicht was man naiv erwarten würde, aber ist das wirklich sinnvolles Verhalten? Man bedenke: Das abgeleitete Objekt wurde zum Zeitpunkt da der Basisklassenkonstruktor läuft noch nicht initialisiert, was bedeutet, dass die Regeln der Sprache selbst verlangen, dass in diesem Fall eine Methode auf einem potentiell ungültigen Objekt aufgerufen wird. Können wir in dieser Situation wirklich nichts sinnvolleres tun? Überlegen wir mal: Das abgeleitete Objekt wurde zum Zeitpunkt da der Basisklassenkonstruktor läuft noch nicht initialisiert, das im Moment in Konstruktion befindliche Objekt kann also noch nicht als ein gültiges Objekt vom abgeleiteten Typ betrachtet werden. Eine virtuelle Methode aus einem Basisklassenkonstruktor in eine abgeleitete Klasse zu dispatchen ist, wenn wir mal ernsthaft drüber nachdenken, eigentlich das so ziemlich am wenigsten sinnvolle Verhalten, das man sich nur vorstellen kann, da es schon rein auf konzeptioneller Ebene unweigerlich die Durchführung einer undefinierten Operation (aufruf einer Methode auf uninitialisiertem Objekt) forciert. Ich würde argumentieren, dass selbst einfach das Verbieten eines solchen Aufrufs per Compile Error wesentlich sinnvoller wäre als das Verhalten von Java und C#. Ginge es noch sinnvoller als das? Nun, was passiert denn während der Konstruktion eines Objektes? Das anfangs komplett uninitialisierte Objekt wird mit jedem Basisklassenkonstruktor zu einem mehr und mehr abgeleiteten Objekt, bis wir am Ende das vollständig abgeleitete Objekt erhalten. Der dynamische Typ des Objektes wird also mit jedem Basisklassenkonstruktor zum mehr und mehr abgeleiteten Typ. Was ist eine virtuelle Methode? Per Definition eine Methode deren Aufruf immer zum final Overrider der Methode im dynamischen Typ des gegebenen Objektes führt. Wie wir gerade festgestellt haben, ändert der dynamische Typ eines Objektes sich logischerweise während der Konstruktion eines Objektes vom am wenigsten abgeleiteten hin zum am meisten abgeleiteten Typ. Der dynamische Typ unseres Objektes in einem Basisklassenkonstruktor ist logischerweise der Typ der Basisklasse in deren Konstruktor wir uns befinden. An dieser Stelle müssen wir uns die Frage stellen, inwiefern das Verhalten von Java und C# nicht eigentlich sogar im Widerspruch zu deren eigenem Typsystem steht, womit es nicht nur wenig sinnvoll sondern vor allem auch noch inkonsistent mit dem Rest der Sprache wäre. Das an diesem Punkt einzige Verhalten, das sowohl konsistent mit dem Konzept von Type an sich als auch der Definition von virtuellen Methoden ist, ist genau das Verhalten das C++ uns gibt, nämlich dass der Aufruf einer virtuellen Methode aus einem Basisklassenkonstruktor die Methode der Basisklasse aufruft, selbst wenn die virtuelle Methode in einer abgeleiteten Klasse überschrieben wurde...
Fazit: Vererbung in C++ hat – imo unverdienterweise – einen schlechten Ruf. Fakt ist, dass Vererbung einfach rein prinzipiell eine extrem komplexe Angelegenheit ist und diese Komplexität sich in einer Sprache, die einen in sich konsistenten Mechanismus dafür anbieten will, notwendigerweise entsprechend widerspiegelt. Komplexität ist, entgegen populärer Erwartungshaltungen, eine einem Problem inhärente Eigenschaft die sich nicht wegzaubern lässt. Der einzige Weg, mit Komplexität umzugehen, ist, sich ihr zu stellen. C++ gibt einem mächtige Werkzeuge in die Hand; es obliegt wie immer der Verantwortung des Programmierers, zu verstehen, was er macht...
Re: Vererbung des Todes.
Verfasst: 13.09.2016, 23:01
von Krishty
Chromanoid hat geschrieben:Beim Implementieren der Methode muss man sich für eine Basisklasse entscheiden
D.h. der Compiler meckert oder wo muss man sich für eine Implementierung entscheiden? Ich hatte die Ausführungen bei drdobbs so verstanden, dass je nach dem wie das Objekt gerade gecastet ist, mal die eine mal die andere Implementierung verwendet wird - also wenn man die Methode als unschuldiges Klässchen von zwei Super-Klassen bekommt und sie nicht selbst implementiert.
Der Artikel erläutert das für den Fall *ohne* virtuelle Vererbung, dann ist es *wirklich* so. Übrigens auch, falls die Klassen überhaupt keine virtuellen Funktionen nutzen (denn dann beruht das komplett auf statischer Typisierung). Starker Unterschied zu Java & D & Co, wo einfach alles immer
virtual entspricht.
Der Gipfel sind übrigens Pointer-to-Member auf solche Typen, also Funktionszeiger auf virtuelle Methoden. Funktionszeiger auf Methoden brauchen ein
this, das übergeben wird. Außerdem brauchen sie die korrekte Vererbungshierarchie um feststellen zu können, wo die Daten liegen. Im Fall von Visual C++ führt das dann dazu, dass Zeiger auf Methoden unterschiedlich groß sind, je nachdem, wie viel gerade über die Klasse bekannt ist (wirklich!). Nachdem ich dann den Code der C-Runtime gesehen habe, der sich darum kümmert, dass Klassen mit virtueller Vererbung als
throw-Parameter vernünftig ins
catch kopiert werden, *ohne* deren Definition zu kennen, habe ich entschieden, beides nicht mehr zu nutzen.
Re: Vererbung des Todes.
Verfasst: 14.09.2016, 00:29
von Chromanoid
@dot: Dass da Java nicht das Gelbe vom Ei ist, ist mir klar. Ich finde C++ da einfach ziemlich unintuitiv. Ich glaube ich hätte das Default-Verhalten genau anders herum gewählt, also Virtual Inheritance per Default - also ganz abgesehen davon was das für technische Implikationen hat. Das wäre ein Fallstrick weniger. Also wenn ich die Ausgaben hier von
http://cpp.sh/9iqsw (normale Mehrfachvererbung) mit
http://cpp.sh/3p7b (virtuelle Vererbung und Fehlermeldung) bzw.
http://cpp.sh/7sr5 (virtuelle Vererbung und expliziter Wahl der Implementierung) vergleiche finde ich letzteres Verhalten wesentlich besser. Das Verhalten ähnelt ja der Mehrfachvererbung von Default-Interface-Implementierungen in Java, vielleicht erwarte ich auch nur deshalb dieses Verhalten.
@Krishty: D.h. Du verwendest in "interfaces" einfach Datenfelder für Funktionspointer und baust Dir damit sozusagen Deine eigene explizite Methoden-Tabelle? (Falls Du überhaupt solche Probleme entwirfst :))
Re: Vererbung des Todes.
Verfasst: 14.09.2016, 00:46
von Krishty
Chromanoid hat geschrieben:(Falls Du überhaupt solche Probleme entwirfst :))
Ich entwerfe sowas üblicherweise anders (ob besser, sei dahingestellt). COM (und damit alles, was mit .NET oder neuen Windows-Schnittstellen kompatibel sein soll) erfordert ein Layout, das ziemlich stark dem virtueller C++-Klassen ähnelt, da komme ich kaum drum herum – aber sowas hilft dem OP nicht wirklich weiter.
Re: Vererbung des Todes.
Verfasst: 14.09.2016, 01:15
von dot
Chromanoid hat geschrieben:Ich finde C++ da einfach ziemlich unintuitiv. Ich glaube ich hätte das Default-Verhalten genau anders herum gewählt, also Virtual Inheritance per Default - also ganz abgesehen davon was das für technische Implikationen hat. Das wäre ein Fallstrick weniger.
Das wäre nicht ein Fallstrick weniger sondern eher 12315834 Fallstricke mehr und würde dem obersten Design-Principle von C++ widersprechen: "You don't pay for what you don't use". Und der Preis für virtual Inheritance ist für C++ Verhältnisse definitiv nicht vernachlässigbar... ;)
Chromanoid hat geschrieben:Das Verhalten ähnelt ja der Mehrfachvererbung von Default-Interface-Implementierungen in Java, vielleicht erwarte ich auch nur deshalb dieses Verhalten.
Nur weil man "Interfaces" in C++ über virtuelle Vererbung ausdrüken kann, heißt nicht, dass virtuelle Vererbung das gleiche ist wie Java Interfaces. Im Gegensatz zu Java kennt C++ eben nicht nur die zwei Spezialfälle von entweder purem abstrakten Interface oder Single-Inheritance sondern verfügt über einen viel allgemeineren Mechanismus zur Vererbung. Niemals aus den Augen verlieren sollte man dabei auch die Tatsache, dass C++ nicht rein auf OOP ausgelegt ist. Traditionelle OOP macht in "modernem" C++ nur einen kleinen und immer kleiner werdenden Teil eines großen Kontinuums aus. Vererbung und vor allem auch Mehrfachvererbung erfüllt in C++ viele Zwecke jenseits von Polymorphismus, insbesondere wenn wir uns in den Bereich der Generischen Programmierung bewegen...
Re: Vererbung des Todes.
Verfasst: 14.09.2016, 10:29
von Chromanoid
dot hat geschrieben:dem obersten Design-Principle von C++ widersprechen: "You don't pay for what you don't use". Und der Preis für virtual Inheritance ist für C++ Verhältnisse definitiv nicht vernachlässigbar... ;)
Naja, zahlen tut man trotzdem, nur eben nicht zur Laufzeit sondern zur Entwicklungs- und Einarbeitungszeit. Ist nicht virtuelle Mehrfachvererbung überhaupt sinnvoll, mir fällt da irgendwie kein Use Case ein? Ich weiß man kann nicht einfach so technisch umschalten, wenn dann plötzlich eine Hierarchie zu einem azyklischen Graphen wird, aber mir geht es ja erst mal nur um die Benutzbarkeit.
dot hat geschrieben:Nur weil man "Interfaces" in C++ über virtuelle Vererbung ausdrücken kann, heißt nicht, dass virtuelle Vererbung das gleiche ist wie Java Interfaces. [...]
Das ist mir schon klar. Ich vergleiche ausschließlich die Benutzbarkeit. ;)