Laufzeit-Polymorphie unterstützende Sprachen
-
Moin, eine kurze Frage an euch.
Was bedeutet der Begriff dynamische Typenbindung bzw. Laufzeit-Polymorphie im Zusammenhang mit (interpretierten) Programmiersprachen, und gibt es diese (oder so etwas Ähnliches ...) auch in C++?
-
@cyborg_beta sagte in Laufzeit-Polymorphie unterstützende Sprachen:
Moin, eine kurze Frage an euch.
Was bedeutet der Begriff dynamische Typenbindung bzw. Laufzeit-Polymorphie im Zusammenhang mit (interpretierten) Programmiersprachen, und gibt es diese (oder so etwas Ähnliches ...) auch in C++?
Das bedeutet, dass erst zur Laufzeit bestimmt wird, auf welchen konkreten Typen sich eine Operation mit einer Objektreferenz (Objektvariable in Java, bzw. Pointer oder Referenz in C++) sich tatsächlich bezieht.
Im Falle von polymorphen Typen ist diese Operation meist der Aufruf einer z.B. in einer abgeleiteten Klasse überschriebene Methode (bzw. "Member-Funktion" in C++-Terminologie) für die dann während der Laufzeit bestimmt wird, für welchen Typen welche Methode letztendlich aufgerufen wird.
Ich habe z.B. eine Referenz auf eine Basisklasse, die jedoch mit einer Referenz auf ein von dieser Basisklasse abgeleitetes Objekt initialisiert wurde:
Base b = new Derived()
.b
hat hier den TypBase
und den dynamischen TypDerived
. Rufe ich nunb.f()
auf, wird letztendlich die Methode/Member-Funktion dieses dynamischen TypenDerived
aufgerufen, obwohl ich über eineBase
-Referenz darauf zugreife - sofernDerived
diese Methode der Basisklasse überschrieben hat (eine eigene Implementation von dieser Funktion hat).Selbstverständlich gibt es das auch in C++. Dort ist aber im Gegensatz zu Java nicht jede Objektvariable eine Referenz und nicht jede Klasse eine polymorphe Klasse, für die eine solche dynamische Bindung stattfindet. In C++ ist eine Klasse genau dann polymorph, wenn sie mindestens eine virtuelle Member-Funktion deklariert oder erbt (
virtual
-Keyword). Auch sollte mindestens noch der Destruktor explizit virtuell sein, selbst wenn nur der Default-Destruktor verwendet wird (z.B. sindstruct A { virtual ~A() = default; }
undstruct B : A {}
polymorphe Klassen).Bezüglich interpretierten Sprachen sehe ich hierbei keine Besonderheiten. M.E. ist es völlig egal wie der Programmcode letztendlich ausgeführt wird - das ist ein Konzept der Sprache selbst und nicht von deren Implementierung. Ich könnte auch einen C++-Interpreter bauen ohne dass sich dadurch irgendwas an der Funktionsweise polymorpher Typen ändern würde.
-
Nein, da fehlt noch etwas.
Leider ist die Begrifflichkeit in der OO-Szene nicht eindeutig geregelt, so dass es regelmäßig zu Problemen kommt. Es gibt verschiedene Formen der Polymorphie.
- statische Polymorphie diese wird zum Übersetzungszeitpunkt aufgelöst
- Laufzeitpolymorphie mit statischer Funktionssignatur, hierbei wird zum Übersetzungszeitpunkt die Funktionssignatur festgelegt, und die Methoden müssen allesamt aus einer Klassenhierachie stammen, sonst funktioniert der Aufruf nicht. Leider wird diese Form der Polymorphie gerne aber auch als dynamisch bezeichnet, so dass es hier zu Doppeldeutigkeiten kommt. C++ gehört zu den Sprachen, die diese Form unterstützen.
- Laufzeitpolymorphie mit dynamischer Funktionssignatur, hier wird erst zur Laufzeit geprüft, ob ein Objekt eine bestimmte Methode unterstützt, und es muss sich dabei nicht um eine interpretierte Sprache handeln siehe Objective-C. Das ganze ist natürlich deutlich langsamer aber erheblich flexibler, weshalb bestimmte Pattern nur so funktionieren.
-
@john-0 sagte in Laufzeit-Polymorphie unterstützende Sprachen:
statische Polymorphie diese wird zum Übersetzungszeitpunkt aufgelöst
Geht das in Richtung Templates und Concepts? So zum Beispiel:
#include <iostream> #include <concepts> // Folgender Code benötigt C++20 template<typename T> concept Duck = requires(T a, int Loudness) { { a.Waddle() } -> std::convertible_to<void>; { a.Croak(Loudness) } -> std::convertible_to<void>; }; class Campbell { public: void Waddle() { std::cout << "Watscheln\n"; } void Croak(int L) { std::cout << "Quak Quak (in der Lautstaerke " << L << ")\n"; } }; void Check(Duck auto d) { d.Waddle(); d.Croak(5); } int main(int argc, char const* argv[]) { Campbell p; Check(p); return 0; }
-
@Quiche-Lorraine sagte in Laufzeit-Polymorphie unterstützende Sprachen:
Geht das in Richtung Templates und Concepts? So zum Beispiel:
Ja, wobei Concepts sind nicht notwendig, sie vereinfachen nur die Fehlersuche, weil die Fehlermeldungen erheblich einfacher lesbar werden. Wenn ich daran zurückdenke was für Fehlermeldungen etablierte C++98 Compiler rausgeworfen haben …
-
@Quiche-Lorraine sagte in Laufzeit-Polymorphie unterstützende Sprachen:
void Check(Duck auto d) { d.Waddle(); d.Croak(5); }
Ich weiss nicht, ob es da eine exakte Definition gibt, wie statische Polymorphie auszusehen hat, aber ich würde sowas durchaus eine Ausprägung eben dieser bezeichnen.
Andere Ausprägungen wären das klassische CRTP oder solche Dinge, die z.B. mit dem expliziten
this
-Parameter in C++23 möglich werden:#include <iostream> struct Base { void print(this const auto& self) { self.print_impl(); } }; struct A : Base { void print_impl() const { std::cout << "A" << std::endl; } }; struct B : Base { void print_impl() const { std::cout << "B" << std::endl; } }; void print(const auto& object) { object.print(); } auto main() -> int { A a; B b; print(a); print(b); }
Output:
A B
-
Danke für eure Antworten!
Mir ist der Unterschied zwischen Punkt 2 (Laufzeit-Polymorphie mit statischer Funktionssignatur, zum Beispiel C++) und Punkt 3 (Laufzeit-Polymorphie mit dynamischer Funktionssignatur, zum Beispiel Java bzw. der Interpreter/die JVM) noch nicht ganz klar. Bei Java ist ja die Besonderheit, dass sowohl übersetzt als auch interpretiert wird ... was vielleicht dafür nicht ein bestes Beispiel darstellt ... leider finde ich auch kaum was zu lesen zu dem Thema.
Ich dachte, in C++ muss der formale (sowie aktuelle) Parametertyp immer bereits zur Laufzeit bekannt (bzw. fest) sein.
-
@cyborg_beta sagte in Laufzeit-Polymorphie unterstützende Sprachen:
Danke für eure Antworten!
Mir ist der Unterschied zwischen Punkt 2 (Laufzeit-Polymorphie mit statischer Funktionssignatur, zum Beispiel C++) und Punkt 3 (Laufzeit-Polymorphie mit dynamischer Funktionssignatur, zum Beispiel Java bzw. der Interpreter/die JVM) noch nicht ganz klar. Bei Java ist ja die Besonderheit, dass sowohl übersetzt als auch interpretiert wird ... was vielleicht dafür nicht ein bestes Beispiel darstellt ... leider finde ich auch kaum was zu lesen zu dem Thema.
Am besten das Reflection Pattern und dessen Umsetzung in verschiedenen Sprachen anschauen, dann wird schnell einsichtigt, wo die Unterschiede liegen. Der Wikipedia Artikel enthält kein C++ Beispiel. Es gibt aber im Netz einige Beispiel z.B. das hier.
Ich dachte, in C++ muss der formale (sowie aktuelle) Parametertyp immer bereits zur Laufzeit bekannt (bzw. fest) sein.
Das ist nur zum Übersetzungszeitpunkt bekannt. Der Compiler optimiert das dann weg und nutzt üblicherweise eine vtable. In der vtable werden die Zeiger der virtuellen Funktionen relativ zu einem Basiszeiger auf die vtable abgelegt. Zur Laufzeit wird dann der Basiszeiger auf die vtable geholt und dann der feste Offset für die beim Übersetzungszeitpunkt bestimmte Methode addiert, und das Ergebnis ohne jede Prüfung ausgeführt.
In anderen Sprachen wird während der Laufzeit nachgeschaut, ob da Objekt eine passende Methode hat mit den passenden Parametern hat. Ist das der Fall wird sie ausgeführt. Ist das nicht der Fall wird meist eine Exception o.ä. ausgelöst.
-
Schauen wir uns ein Beispiel aus Java an:
public class AClazz<T> { @SuppressWarnings("unchecked") public T add(T a, T b) { if (a instanceof Number n1 && b instanceof Number n2) { return (T) (Double) ((Double) n1 + (Double) n2); } return null; } public String add(String a, String b) { return a + b; } public static void main(String[] args) { AClazz<String> clazzA = new AClazz<>(); // Das compiliert! AClazz<Integer> clazzB = new AClazz<>(); AClazz<Double> clazzC = new AClazz<>(); // System.out.println(clazzA.add("hallo", "ihr")); // Ambiguous method call. Both add(String,String) in AClazz and add(String,String) in AClazz match System.out.println(clazzB.add("hallo", "du")); // System.out.println(clazzB.add(42, 1)); // Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Double (java.lang.Integer and java.lang.Double are in module java.base of loader 'bootstrap') System.out.println(clazzC.add(12., .34)); } }
Welch Wunder, das compiliert und lässt sich starten. Die Ausgabe ist:
hallodu 12.34
Aber damit noch nicht genug, wenn man Zeile 19 einkommentieren würde, dann könnte es nicht mehr compiliert werden. Wenn man Zeile 24 einkommentieren würde, dann könnte es zwar compiliert werden, aber es gebe einen Laufzeitfehler.
Ich weiß nicht, ob das in C++ so ähnlich wäre ... Aber ich vermute, dass Java deshalb nicht (sehr) streng typisiert ist.
Schauen wir uns noch das an, was der Compiler daraus macht: (
javap -c <...>
)Compiled from "AClazz.java" public class AClazz<T> { public AClazz(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public T add(T, T); Code: 0: aload_1 1: instanceof #7 // class java/lang/Number 4: ifeq 45 7: aload_1 8: checkcast #7 // class java/lang/Number 11: astore_3 12: aload_2 13: instanceof #7 // class java/lang/Number 16: ifeq 45 19: aload_2 20: checkcast #7 // class java/lang/Number 23: astore 4 25: aload_3 26: checkcast #9 // class java/lang/Double 29: invokevirtual #11 // Method java/lang/Double.doubleValue:()D 32: aload 4 34: checkcast #9 // class java/lang/Double 37: invokevirtual #11 // Method java/lang/Double.doubleValue:()D 40: dadd 41: invokestatic #15 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 44: areturn 45: aconst_null 46: areturn public java.lang.String add(java.lang.String, java.lang.String); Code: 0: aload_1 1: aload_2 2: invokedynamic #19, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 7: areturn public static void main(java.lang.String[]); Code: 0: new #23 // class AClazz 3: dup 4: invokespecial #25 // Method "<init>":()V 7: astore_1 8: new #23 // class AClazz 11: dup 12: invokespecial #25 // Method "<init>":()V 15: astore_2 16: new #23 // class AClazz 19: dup 20: invokespecial #25 // Method "<init>":()V 23: astore_3 24: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream; 27: aload_2 28: ldc #32 // String hallo 30: ldc #34 // String du 32: invokevirtual #36 // Method add:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 35: invokevirtual #39 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 38: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream; 41: aload_3 42: ldc2_w #45 // double 12.0d 45: invokestatic #15 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 48: ldc2_w #47 // double 0.34d 51: invokestatic #15 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 54: invokevirtual #49 // Method add:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; 57: invokevirtual #52 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 60: return }
Zum einen werden bei den drei Initialisierungen die Typ-Parameter der Klasse entfernt (oder durch
Object
ersetzt), zum anderen sehen alle drei Instanzen vonAClazz
gleich aus. Sprich, zur Laufzeit sieht die VM die Typen nicht mehr, und es fallen einige Checks weg.Interessant wäre nun zu wissen, an welcher Stelle die (dynamische) Laufzeit-Polymorphie hier greift. Da bin ich unsicher.
-
Java ist nicht mein Ding. EOT
-
@john-0 sagte in Laufzeit-Polymorphie unterstützende Sprachen:
Java ist nicht mein Ding.
Deshalb fragte ich ja explizit nach Unterschieden zu C++ ... EOT.
-
@cyborg_beta sagte in Laufzeit-Polymorphie unterstützende Sprachen:
54: invokevirtual #49 // Method add:(Ljava/lang/Object;Ljava
Interessant wäre nun zu wissen, an welcher Stelle die (dynamische) Laufzeit-Polymorphie hier greift. Da bin ich unsicher.
Ich hab auch nur wenig Ahnung von Java, aber wenn ich raten müsste, dann überall dort, wo die VM-Instruktion
invokevirtual
ausgeführt wird. Der Begriff Virtuelle Funktion/Methode ist schliesslich nicht C++-spezifisch, auch wenn Java im Gegensatz zu C++ keinvirtual
-Schlüsselwort hat.