Laufzeit-Polymorphie unterstützende Sprachen



  • 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
    

  • Gesperrt

    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.


  • Gesperrt

    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 von AClazz 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


  • Gesperrt

    @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++ kein virtual-Schlüsselwort hat.


Anmelden zum Antworten