Verständnis Exceptions & Mehrfachvererbung



  • Hi!

    Mich interessiert schon seit längerem, wie in C++ intern Exceptions und Mehrfachvererbung gehandhabt werden. Ich gehe mal davon aus, dass das je nach Compilerkrams unterschiedlich gehandhabt wird, aber ich vermute, dass es da eine "generelle" Lösung gibt.

    Bei Mehrfachvererbung hab ich schon Gewisse Vorstellungen, wie das laufen könnte, ich weiß es aber nicht 😞

    • Erstmal zum Verständnis von Klassen-Handhabung allgemein, ich nehme mal eine Beispielklasse, z.B. jene:
    class ClassA
    {
        int x, y;
        void Do( int z) { x=y+z; }
    };
    ...
    ClassA a;
    a.Do();
    
    // Kann es sein, dass der Compiler daraus in etwa folgenden Code erstellt?:
    
    struct ClassA_Members
    {
        int x, y;
    };
    
    void ClassA_Do( ClassA_Members* pThis, int z )
    { pThis->x = pThis->y + z; }
    
    // Was quasi folgendem entspräche?:
    
    void ClassA_Do( void* pClass, int z )
    {
        int* pX = (int*)pClass;
        int* pY = (int*) ((char*)pClass+4);
        *px = *pY+z;
    }
    

    Wenn man wie im Beispiel eine Instanz der Klasse erstellt, dann wird einfach der Stack-Zeiger um 8 hochgepusht, oder? Ähnlich, wie er bei einer Instanz eines Integers um 4 hochgepusht wird.
    Wenn man jetzt die Funktion "a.Do()" aufruft, übergibt er den Stack-Zeiger, wo die Klasse liegt.
    Wenn man die Instanz mit "new" erstellt, wird entsprechend der Pointer übergeben.
    Und der Zeiger auf die Klasse müsste gleich dem Zeiger auf den ersten Member (x) sein?
    Stimmt das soweit?

    • Also, erst zur Mehrfachvererbung (ohne virtuelle Funktionen):
      Wenn der Punkt oben stimmt, denke ich mir das so:
    class ClassA
    {
        int x, y;
        void Do( int z) { x=y+z; }
    };
    
    class ClassB
    {
        int v;
        void Bla() {...}
    };
    
    class ClassC : public ClassA, ClassB
    {
        int i;
        void Blubb() { ... }
    };
    
    ...
    ClassC* c = new ClassC();
    c->Do( 100 );  // => ClassA_Do( (ClassA*)c, 100 ); von dem Pointer benutzt/kennt er nur die ersten 8 Bytes / 2 Ints.
    c->Bla();      // => ClassB_Bla( (ClassB*) ((char*)c+8) ); damit quasi der Pointer auf ClassB::v übergeben wird.
    c->Blubb();    // => ClassC_Blubb( c ); damit er "alle" daten kennt.
    

    Stimmen die 3 Kommentare? Irgendwie krieg ichs nicht hin, das alles mit zusammenhängendem Text zu (be-)schreiben, halb Code, halb Kommentar klappt irgendwie besser 😕

    • Dann zu den virtuellen Funktionen:
      Wenn eine Klasse mindestens eine virtuelle Funktion besitzt, bekommt sie als "ersten Member" eine Pointer-Variable, die auf eine Liste von Funktions-Zeigern ist? Bei folgendem Konstrukt
    class ClassA
    {
        int x, y;
        virtual void Do( int z) { x=y+z; }
    };
    
    class ClassD : public ClassA
    {
        virtual void Do( int z) { x=y-z; }
    };
    

    würde der Konstruktor der Klasse ClassD dann den Funktionszeiger auf die "globale" Funktion ClassD_Do(int) setzen? Und wenn die Funktion dann mit ((ClassA)pointerToD)->Do(100)* aufgerufen wird, wird in der Liste der Pointer auf die richtige Funktion rausgesucht, welche dann aufgerufen wird. Stimmt das auch noch?

    Was ich jetzt nicht verstehe, ist, wie Mehrfachvererbung von virtuellen Funktionen funktioniert. Wird da der VTable (Zeiger auf die Liste der Zeiger auf die virtuellen Funktionen) mit allen geerbten virtuellen Funktionen gefüllt? Oder wird für jede geerbte Klasse wieder vor den Daten ein eigener VTable angelegt, so dass der Zeiger auf die Instanz wieder in einzelne Zeiger der geerbten Klassen "übersetzt" werden kann?
    Und wieso können Sprachen wie C# oder Java keine Mehrfachvererbung? Hat das was mit dem Zwischencode zu tun, dass er zu aufwendig würde?

    • Und zum Schluss Exceptions:
      Hier verstehe ich überhaupt nicht, wie gearbeitet wird. Nehmen wir folgenden Code an:
    void ItThrows()
    {
        ...
        throw new Exception( "" );
        ...
    }
    
    void ItCatches()
    {
        try { ... ItThrows(); ... }
        catch ( Exception* ) { }
    }
    

    Wie schafft es dann die Exception, zur anderen Funktion wieder zurück zu kommen? "ItThrows" müsste ja von "ItCatches" mit einem call aufgerufen werden. Wenn nun die Ausnahme ausgelöst wird, was passiert dann? Wie kommt er zu "ItCatches" zurück, bzw wie nimmt er dabei den Exception-Typ und das Exception-Objekt mit?

    Fragen über Fragen... Bzw, beim ersten Teil will ich ja fast nur wiederlegt oder bestätigt werden... Bin jedenfalls für jede Antwort dankbar und gespannt 🙂

    Edit: Oh Mist 😕 Ich hätte ja eigentlich auch ein wenig testen können 😞

    Edit2: Ok, hab bissl getestet... Das mit der einfachen Vererbung und Mehrfachvererbung ist jetzt halbwegs klar, obwohl ichs gern trotzdem bestätigt hätte 😃 Und bei virtueller Mehrfachvererbung gibt es dann ein paar VTables, wenn ich mich nicht irre...

    Edit3: Exceptions sind mir aber nach wie vor völlig fremd 🤡



  • Allgemein: Vieles von dem, was du da beschreibst, geht in die Interna eines Compilers 😉

    Zu den Fragen:
    Wenn man von Allignment-Problemen mal absieht, stimmt die Aufteilung bei einer "normalen" Klasse in etwa. Bei Vererbung kommen (normalerweise) erst die Member der Basisklasse und danach die eigenen, bei Mehrfachvererbung nacheinander die Member aller Basisklassen (wenn du noch virtuelle Vererbung dazubekommst, wird es richtig kompliziert).

    Wenn die Klasse virtuelle Methoden hat, kommt zu den normalen Membern noch der vptr dazu (wo genau, weiß nur der Compiler). Bei Mehrfachvererbung bekommst du vermutlich von beiden Eltern einen vptr und der Compiler muß dann auflösen, ob die vom Vater oder der Mutter geerbte Methode betroffen ist.

    @Exceptions: Die Exception (bei deinem Beispiel der Zeiger auf den mit new angeleten Speicher) wird vom throw in einen eigenen Speicherbereich zwischengelagert, bevor der Stack bis zum passenden catch aufgeräumt wird. Anschließend erhältst du entweder eine Referenz oder Kopie dieses Wertes in den catch-Block übergeben.
    (übrigens solltest du Exceptions eher als Wert werfen und per Referenz auffangen)



  • Na gut, danke für die Antwort! Ich werd aber noch ein wenig forschen 🙂


Anmelden zum Antworten