Kapitel 15 - Polymorphie

Peter Ulbrich

🚀 by Decker

Einleitung

  • Bisher: Dank Vererbung akkurate Abbildung der RealitĂ€t in Klassenhierarchie
  • Jetzt: Polymorphie đŸ€”

Vererbung: Status Quo

  • Zeiger einer Basisklasse wird Objekt von Kindklasse zugewiesen
Base * b = new Child(); // bzw. mit unique_ptr
b->do_stuff();
  • Bisheriges Problem: Bindung der Methoden geschieht wĂ€hrend des Übersetzens

    → „Methode do_stuff() aus Oberklasse Base wird aufgerufen”

Vererbung: Status Quo

  • Bisheriges Problem: Bindung der Methoden an Objekte geschieht wĂ€hrend des Übersetzens
  • Jetzt: Polymorphie/Polymorphismus

    → Vielgestaltigkeit

    • Technik zur Bindung von Methoden zur Laufzeit (dynamische Bindung)
    • Erst zur Laufzeit steht fest, welche Methode aufgerufen wird
    • Heißt daher auch Dynamic Dispatch
#include <iostream>
using std::cout, std::endl;

class Base {
public:
    void print() { cout << "Base" << endl; }
};

class Child: public Base {
public:
    void print() { cout << "Child" << endl; }
};

int main() {
    Base * b = new Child();
    b->print();
    delete b;
}
cpp

Beispiel - Polymorphie

  • Ziel: dynamische Auswahl von ausgabe() fig/uml-polymorphism-basic-example.svgPolymorphismus bei FrĂŒchten
class Frucht {
protected:
    std::string name;
public:
    void ausgabe();
};

class Huelsenfrucht: public Frucht {
public:
    void ausgabe();
};
class Obst: public Frucht {
public:
    void ausgabe();
};
class Suedfrucht: public Obst {
public:
    void ausgabe();
};

So geht es natĂŒrlich noch nicht!

Virtuelle Methoden

  • Methoden können dynamisch gebunden werden

    → Auswahl zur Laufzeit

  • SchlĂŒsselwort virtual in C++

  • Virtuelle Methoden können ĂŒberschrieben werden

    → Die Auswahl erfolgt nun fĂŒr diese Methode dynamisch

class Frucht {
protected:
    std::string name;
public:
    virtual void ausgabe();
};

class Huelsenfrucht: public Frucht {
public:
    void ausgabe();
};
class Obst: public Frucht {
public:
    void ausgabe();
};
class Suedfrucht: public Obst {
public:
    void ausgabe();
};

Beispiel - Virtuelle Methode

#include <iostream>

using std::cout, std::endl;

class Frucht {
public:
    virtual void ausgabe() {
        cout << "F" << endl;
    };
    virtual ~Frucht() {};
};

class Obst: public Frucht {
public:
    void ausgabe() {
        cout << "O" << endl;
    };
};

class Suedfrucht: public Obst {
public:
    void ausgabe() {
        cout << "SF" << endl;
    };
};

int main() {
    Frucht * frucht = new Frucht();
    Frucht * obst = new Obst();
    Frucht * suedfrucht = new Suedfrucht();

    frucht->ausgabe();
    obst->ausgabe();
    suedfrucht->ausgabe();

    delete frucht;
    delete obst;
    delete suedfrucht;
}
cpp

Destruktor mit virtual

  • Konstruktoren können nicht virtuell sein
  • Destruktoren hingegen sollten virtuell sein
    • Ansonsten: statische Festlegung wĂ€hrend des Übersetzens
    • Resultat: Aufruf des Destruktors der Basisklasse (und ggf. undefiniertes Verhalten)
  • Empfehlung: Markiert Destruktor von Basisklassen immer als virtual
#include <iostream>
using std::cout, std::endl;

class Base {
public:
    virtual ~Base() {
        cout << "Base" << endl;
    }
};
class Child : public Base {
public:
    ~Child() {
        cout << "Child" << endl;
    }
};

int main() {
    Base * b = new Child();
    delete b;
}
cpp

Explizites Überschreiben – override

  • Überschreiben einer virtuellen Methode in Kindklasse mit override explizit
  • Syntax: void print() override;
  • Erzeugt Compiler-Warnung, wenn nicht ĂŒberschrieben wird
  • Verwendung ist (leider) nicht zwingend von C++ gefordert 🙁
  • Erhöht die Lesbarkeit aber ungemein. Nutzt es!
class Frucht {
protected:
    std::string name;
public:
    virtual void ausgabe();
};

class Obst: public Frucht {
public:
    void ausgabe() override;
};

class Suedfrucht: public Obst {
public:
    void ausgabe() override;
};

Das letzte Wort – final

  • Analog zu override: SchlĂŒsselwort final
  • Verhindert weiteres Überschrieben einer Methode
  • Erzeugt Compiler-Fehler bei Versuch zu ĂŒberschreiben
class Frucht {
protected:
    std::string name;
public:
    virtual void ausgabe();
};

class Obst: public Frucht {
public:
    void ausgabe() final;
};

class Suedfrucht: public Obst {
public:
    void ausgabe() override; // ERROR
};

Diamond-Shape-Problem

  • Das Diamond-Shape-Problem bezeichnet eine spezielle Konstellation der Vererbung
    • Zwei Eltern erben von einer gemeinsamen Klasse und aus beiden Eltern wird eine Kindklasse abgeleitet
    • Das Klassendiagramm sieht einer Raute bzw. Diamanten Ă€hnlich (→ Name)

Diamond-Shape

fig/diamond-shape-problem.svg

TatsÀchliche Vererbungshierarchie

fig/multiple-inheritance-instead-of-diamond-shape.svg
  • Problem: Beide Elternteile haben bei der regulĂ€ren Mehrfachvererbung jeweils eine eigene Instanz von Base

Diamond-Shape-Problem

  • Problem: Bei Aufruf ist unklar, welches print() verwendet werden soll → Error
    • Mother::print(); // Dies?
    • Father::print(); // Oder dies?
  • Lösung: Bei der Klassendeklaration der Eltern virtuell von Base ableiten
    • Erzeugt eine geteilte Basisklasse anstatt zwei separaten Instanzen
# include <iostream>
using std::cout, std::endl;

class Base {
public:
    virtual void print() {
        cout << "Base" << endl;
    }
};

class Mother: virtual public Base {};
class Father: virtual public Base {};
class Child : public Mother, public Father {};

int main() {
    Child c;
    c.print(); // Ok mit virtueller Ableitung
}
cpp

Rein virtuelle Methoden

  • Ebenfalls möglich: rein virtuelle Methoden zu deklarieren (pure virtual)
    • In C++: virtual void print() = 0; // = 0 deklariert Funktion als pure
  • Bedeutung: Implementierung in dieser Klasse nicht vorhanden
  • Folgen
    • Erzeugen einer Instanz der Basisklasse nicht möglich
    • Methode muss in allen abgeleiteten Klassen implementiert werden
  • Klassen mit rein virtuellen Methoden heißen abstrakte Klassen
    • HĂ€ufige AbkĂŒrzung: Abstract Base Class (ABC)
    • Alternative Bezeichung: Interface (wird oft synonym genutzt)

Beispiel - rein virtuelle Methoden

  • Unterschiede zu regulĂ€ren Klassen
    • Abstrakte Klassen können nicht initialisiert werden
    • Dienen als Vorlage zur Konstruktion anderer Klassen
    • Definiert Set von Methoden, das in allen abgeleiteten Klassen verfĂŒgbar und implementiert ist
# include <iostream>
using std::cout, std::endl;

class Form {
public:
    virtual void print_area() = 0;
};

class Quadrat : public Form {
    double x_;
    double y_;
public:
    Quadrat(double x, double y) : x_(x), y_(y) {}
    void print_area() override {
        cout << x_ * y_ << endl;
    }
};

class Kreis : public Form {
    double r_;
public:
    Kreis(double radius) : r_(radius) {}
    void print_area() override {
        cout << r_ * 3.14 << endl;
    }
};

int main() {
    Form * quadrat = new Quadrat(2.5, 3.0);
    Form * kreis = new Kreis(1.0);

    quadrat->print_area();
    kreis->print_area();
}
cpp

Vtables - Ein Blick hinter die Kulissen

  • Die wichtigsten Punkte der Polymorphie sind damit abgeschlossen ✅
  • Abschließend ein Blick hinter die Kulissen: Vtables

  • Vtables steht fĂŒr Virtuelle Tabellen

  • Jede Klasse mit virtuellen Funktionen bekommt eine zusĂ€tzliche Member-Variable

    → Zeiger auf Tabelle mit Funktionen

  • Achtung: Vtables sind streng genommen nicht im C++-Standard definiert
    • Snd ein Implementierungsdetail
    • Allerdings sehr weit verbreitet (De-Facto-Standard)

Vtables

  • Einfache Umsetzung

    • Compiler erstellt Vtable
    • FĂŒgt automatisch neuen Zeiger auf Vtable ein – hier: __vptr
  • Zur Laufzeit: Nachschlagen in der Tabelle

  • Zeiger in Tabelle verweist auf konkrete Implementierung in Klasse

  • Mehrkosten: Einige Dereferenzierungen von Zeigern

    → Ziemlich schnell

Beispiel - Vtables im Clang-Compiler

  • Code und Analyse-Ausgabe des Clang-Compilers
    • Erzeugt mit: clang++ -Xclang -fdump-vtable-layouts vtable.cpp
#include <iostream>
using std::cout, std::endl;

class Frucht {
   public:
    virtual void ausgabe() { cout << "F" << endl; }
};

class Obst : public Frucht {
   public:
    void ausgabe() override { cout << "O" << endl; }
};

class Suedfrucht : public Obst {
   public:
    void ausgabe() override { cout << "SF" << endl; }
};

int main() {
    Frucht f;
    Obst o;
    Suedfrucht sf;

    f.ausgabe();
    o.ausgabe();
    sf.ausgabe();
}
Vtable for 'Frucht' (3 entries).
   0 | offset_to_top (0)
   1 | Frucht RTTI
       -- (Frucht, 0) vtable address --
   2 | void Frucht::ausgabe()

VTable indices for 'Frucht' (1 entries).
   0 | void Frucht::ausgabe()

Vtable for 'Obst' (3 entries).
   0 | offset_to_top (0)
   1 | Obst RTTI
       -- (Frucht, 0) vtable address --
       -- (Obst, 0) vtable address --
   2 | void Obst::ausgabe()

VTable indices for 'Obst' (1 entries).
   0 | void Obst::ausgabe()

Vtable for 'Suedfrucht' (3 entries).
   0 | offset_to_top (0)
   1 | Suedfrucht RTTI
       -- (Frucht, 0) vtable address --
       -- (Obst, 0) vtable address --
       -- (Suedfrucht, 0) vtable address --
   2 | void Suedfrucht::ausgabe()

VTable indices for 'Suedfrucht' (1 entries).
   0 | void Suedfrucht::ausgabe()

Ausblick - Statische Reflexion

  • Bisher nicht besprochen: Reflexion

  • Eigenschaft vieler OOP-fĂ€higen Programmiersprachen, z. B. Java

  • Bezeichnet FĂ€higkeit, Informationen ĂŒber sich selbst abzurufen

    → z.B. eigener Typ oder Member inkl. Namen

// In Java möglich:

public class Person {
    private String name;
}
// ...

Object p = new Person();
if (p.getClass() == Person.class) {
// Weitere Verarbeitung von Person
}
  • In C++ geht das leider (noch) nicht 🙁
  • Kommender Standard (C++26 oder 29) wird statische Reflexion erlauben; genauer Zeitplan fehlt aber

Zusammenfassung

  • Modellierung der RealitĂ€t mittels Klassenhierarchie – siehe Frucht-Beispiel

  • Vorteile von Polymorphie

    • Verwaltung von Objekten einer Basisklasse
    • Dennoch Zugriff auf Verhalten der Unterklasse
  • Realisierung von Polymorphie mit SchlĂŒsselword virtual

  • Erzwingen der Implementierung mittels abstrakter Klassen

    → Methoden als pure virtual definieren

  • Implementierung mittels vtables

Logo Sys
Kapitel 15 - Polymorphie