Vererbung des Todes.

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Vererbung des Todes.

Beitrag 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.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4256
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Vererbung des Todes.

Beitrag 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.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Vererbung des Todes.

Beitrag 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...
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Vererbung des Todes.

Beitrag 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.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4256
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Vererbung des Todes.

Beitrag 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 :))
Benutzeravatar
Krishty
Establishment
Beiträge: 8238
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Vererbung des Todes.

Beitrag 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.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Vererbung des Todes.

Beitrag 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...
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4256
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Vererbung des Todes.

Beitrag 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. ;)
Antworten