Ganz schnelle Frage: Was passiert hier genau?



  • class A
    {
       public:
          virtual void foo()=0;
    };
    
    class B
    {
       public:
          void foo() {...}
    };
    

    Die Frage ist: Was genau bedeutet es, wenn ich foo in der abgeleiteten Klasse das virtual-Attribut wegnehme? Ist die Methode dann final?



  • Noch was vergessen... wie sieht's bei nicht-virtuellen Destruktoren aus? Damit habe ich mich auch noch nie wirklich beschäftigt.



  • GanzSchneller schrieb:

    class A
    {
       public:
          virtual void foo()=0;
    };
    
    class B
    {
       public:
          void foo() {...}
    };
    

    Die Frage ist: Was genau bedeutet es, wenn ich foo in der abgeleiteten Klasse das virtual-Attribut wegnehme? Ist die Methode dann final?

    Nö, sobald in einer Klasse virtual steht, ist die Funktion automatisch in allen abgeleiteten Klassen virtual, d. h. es macht keinen Unterschied, ob du das virtual in B nochmal explizit hinschreibst (ich würds aber machen).



  • Dann beschaeftige dich damit. Zu deiner Frage: Einmal virtual immer virtual . Jedoch ist es guter Stil, bei virtuellen Methoden auch virtual davorzuschreiben, damit nicht staendig Code in Basisklassen nachgeschlagen werden muss.



  • GanzSchneller schrieb:

    Die Frage ist: Was genau bedeutet es, wenn ich foo in der abgeleiteten Klasse das virtual-Attribut wegnehme?

    Gar nix. Mit und ohne virtual in der abgeleiteten Klasse ist völlig egal. Man schreibt es davor (oder auch nicht), damit jedem Betrachter klar ist, daß diese Methode virtuell ist, technisch macht es aber keinen Unterschied.
    Wenn in einer Klasse eine virtuelle Methode vorhanden ist, sollte auch der Destruktor virtuell sein, damit beim Zerstören eines Objektes auch wirklich die Destruktor - Hierarchie beachtet wird.
    Auch hier genügt rein technisch das virtual in der Basisklasse.



  • Ach... das ist "nur" eine stilistische Angelegenheit!? Na sowas, ich dachte, das hätte auch Auswirkungen auf den Compiler...

    Super, danke für die schnellen Antworten! Dann hat sich das mit den Destruktoren eigentlich auch schon erledigt, wenn ich so drüber nachdenke.


  • Mod

    Belli schrieb:

    Wenn in einer Klasse eine virtuelle Methode vorhanden ist, sollte auch der Destruktor virtuell sein, damit beim Zerstören eines Objektes auch wirklich die Destruktor - Hierarchie beachtet wird.

    ⚠ Auch wenn man keine virtuellen Funktionen hat, kann ein virtueller Destruktor eine gute Idee sein. Sonst werden bei Sachen wie

    Base *foo=new Derived;
    delete foo;
    

    nicht die richtigen Destruktoren aufgerufen.



  • Ja, da hab ich zu schnell so aus dem Ärmel geantwortet. Es muß wohl heißen:
    Wenn eine Klasse als Basisklasse für eine andere dienen kann, sollte der Destruktor virtuell sein.
    Naja, und falls nicht, schadet es auch nichts.



  • Belli schrieb:

    Ja, da hab ich zu schnell so aus dem Ärmel geantwortet. Es muß wohl heißen:
    Wenn eine Klasse als Basisklasse für eine andere dienen kann

    und abgeleitete Klassen nicht-statische Member haben können 😉

    Naja, und falls nicht, schadet es auch nichts.

    Ein wenig Performance, wenn sonst keine vtable nötig wäre.

    Aber ich machs im Prinzip genau so, wie du sagst: Sobald ne Klasse ne Basisklasse ist, wird der Destruktor virtuell.



  • Michael E. schrieb:

    Sobald ne Klasse ne Basisklasse ist, wird der Destruktor virtuell.

    Oder protected.



  • SeppJ schrieb:

    ⚠ Auch wenn man keine virtuellen Funktionen hat, kann ein virtueller Destruktor eine gute Idee sein. Sonst werden bei Sachen wie

    Base *foo=new Derived;
    delete foo;
    

    nicht die richtigen Destruktoren aufgerufen.

    Was für einen Grund gibt es, einen besitzenden Basisklassenzeiger auf eine abgeleitete Klasse zu deklarieren, ausser um virtuelle Methoden aufzurufen?

    Belli schrieb:

    Wenn eine Klasse als Basisklasse für eine andere dienen kann, sollte der Destruktor virtuell sein.

    Michael E. schrieb:

    Aber ich machs im Prinzip genau so, wie du sagst: Sobald ne Klasse ne Basisklasse ist, wird der Destruktor virtuell.

    Ihr bringt Vererbung mit Polymorphie durcheinander – wie leider viele Leute. Dabei impliziert das erste noch lange nicht das zweite. In der Tat gibt es ab und zu Fälle, wo man nur Vererbung braucht. Ich habe das sogar relativ oft. Und wieso für Dinge bezahlen, die man nicht braucht? Um keine Fälle unterscheiden zu müssen?

    Ich sehe das anders: Durch jenes Abwägen fällt man eine Designentscheidung, die weiter reicht als nur "VTable oder nicht". Man deutet mit fehlendem virtuellen Destruktor an, dass die Klasse nicht zur polymorphen Nutzung gedacht ist. Zum Beispiel, wenn Wertsemantik wichtiger ist und es sich nicht lohnt, beide Konzepte zu vereinen.

    brotbernd schrieb:

    Oder protected.

    Auch das nicht. Wer sagt, dass Basisklassen grundsätzlich nicht direkt instanziiert (und damit zerstört) werden sollen?



  • Nexus schrieb:

    Michael E. schrieb:

    Aber ich machs im Prinzip genau so, wie du sagst: Sobald ne Klasse ne Basisklasse ist, wird der Destruktor virtuell.

    Ihr bringt Vererbung mit Polymorphie durcheinander – wie leider viele Leute.

    Nö.

    Dabei impliziert das erste noch lange nicht das zweite. In der Tat gibt es ab und zu Fälle, wo man nur Vererbung braucht. Ich habe das sogar relativ oft.

    Bei mir ist das so selten, dass ich meine Generalisierung für mich ruhig stehen lassen kann. Wo brauchst du das denn so häufig?

    brotbernd schrieb:

    Oder protected.

    Auch das nicht. Wer sagt, dass Basisklassen grundsätzlich nicht direkt instanziiert (und damit zerstört) werden sollen?

    Auch hier dürften die Anwendungsfälle wieder vernachlässigbar wenig sein, oder?



  • Michael E. schrieb:

    Bei mir ist das so selten, dass ich meine Generalisierung für mich ruhig stehen lassen kann. Wo brauchst du [Vererbung ohne Polymorphie] denn so häufig?

    Sicher mal für Mikro-Klassen wie NonCopyable . Dann bei Sachverhalten, in denen eine abgeleitete Klasse lediglich eine Erweiterung ist, aber kein Verhalten hinzufügt, das von der Basisklasse aus angesprochen werden soll. Eine Klasse in einem Projekt von mir:

    // Countdown-Maschine
    class Timer { ... };
    
    // Countdown-Maschine, die bei t=0 eine Funktion aufruft
    class TriggeringTimer : public Timer { ... };
    

    Im Weiteren eine "Funktionalitätsklasse". Ich erbe von ihr, um eine Schnittstelle zu erhalten. Allerdings ist diese nicht abstrakt, sondern gleich mit Verhalten belegt, das in der Basisklasse vollständig definiert ist, also ganz ohne virtuelle Methoden. Die Klasse kann als Subtyp der abgeleiteten Klasse angesprochen werden, um Einstellungen am konkreten Objekt zu ändern. Das geht also gewissermassen in Richtung Interface. Aus einem aktuellen anderen Thread hier:

    // Speichert String und stellt entsprechende Schnittstelle bereit
    class StringAttributable { ... };
    
    // Beinhaltet String-Attribut
    class Button : public StringAttributable { ... };
    

    Allerdings sehe ich diese Technik sonst nicht so häufig. Mit CRTP kombiniert habe ich auch was:

    // Speichert Klick-Listener und bietet entsprechende Methoden an
    template <class GuiComponent>
    class Clickable { ... };
    
    // Benutzt Listener-Schittstelle und -Implementierung
    class Button : public Clickable<Button> { ... };
    

    Die erst in der abgeleiteten Klasse vorhandene Funktionalität kann über statische Downcasts von this angesprochen werden.

    Für Vererbung ohne Polymorphie wären Policy-Klassen in der Template-Programmierung ein weiteres Beispiel. Es gibt also schon einige Situationen, an die man bei Vererbung vielleicht gar nicht direkt denkt. Ich benutze natürlich auch oft Laufzeitpolymorphie, aber eben noch lange nicht immer im Zusammenhang mit Vererbung.



  • Zum Timer: Überdeckst du in TriggeringTimer eine Funktion von Timer in der Hoffnung, dass kein Benutzer eine Collection von (Triggering)Timers erstellen will?

    Die Funktionalitätsklasse kann ich mir leider auch noch nicht vorstellen, tut mir Leid.

    Zum Button: Dann les ich mal den anderen Thread durch.



  • Michael E. schrieb:

    Zum Timer: Überdeckst du in TriggeringTimer eine Funktion von Timer in der Hoffnung, dass kein Benutzer eine Collection von (Triggering)Timers erstellen will?

    Nein, ich definiere keine Methode in TriggeringTimer neu, die schon in Timer definiert wurde. Das wäre alleine schon wegen impliziten Upcasts gefährlich (Subtyping). Wenn ich ein TriggeringTimer als Timer ansprechen will – was ja keine dynamische Polymorphie erfordert – könnte sonst bei nicht-virtuellen Funktionen anderes Verhalten als beim direkten Funktionsaufruf zustande kommen.

    Michael E. schrieb:

    Die Funktionalitätsklasse kann ich mir leider auch noch nicht vorstellen, tut mir Leid.

    Hm, was kannst du denn nicht nachvollziehen? Wie gesagt, das Design scheint nicht sehr verbreitet zu sein. In meinen Augen ist es teilweise aber recht praktisch.



  • Nexus schrieb:

    Michael E. schrieb:

    Die Funktionalitätsklasse kann ich mir leider auch noch nicht vorstellen, tut mir Leid.

    Hm, was kannst du denn nicht nachvollziehen? Wie gesagt, das Design scheint nicht sehr verbreitet zu sein. In meinen Augen ist es teilweise aber recht praktisch.

    Hast du ein halbwegs sinnvolles Beispiel parat?



  • Michael E. schrieb:

    Hast du ein halbwegs sinnvolles Beispiel parat?

    Die "Funktionalitätsklasse" sollte sich auf das anschliessend gepostete Codebeispiel mit dem Button beziehen. Sorry, falls das zu wenig zusammenhängend war. Falls das Beispiel nicht sinnvoll ist, wäre ich froh zu wissen warum 🙂

    Wichtig ist halt, dass es sich bei den genannten Basisklassen nicht um abstrakte Klassen handelt, sondern um Klassen ohne virtuelle Methoden, von deren Schnittstelle und Implementierung die abgeleitete Klasse profitiert. Da sie dennoch nicht wirklich eigenständig sind, sondern primär Funktionalität für andere Klassen bereitstellen, nenne ich sie "Funktionalitätsklassen" 😉



  • Ok du redest von öffentlicher Vererbung zum Zweck der Wiederverwendung von bestehenden Implementierungen. So verwendet dein TriggeredTimer das Zeugs von Timer indem er davon erbt, was weniger Schreibarbeit bedeutet, als an eine Timer Instanz zu delegieren. Das ist zwar praktisch, birgt aber die Gefahr, dass jemand auf die Idee kommen könnte diese Klassen polymorph zu verwenden. Daher benutze ich sowas eigenltich nur noch, wenn der Kram nicht öffentlich ist. Wenn irgendwie sicher gestellt werden kann, dass so eine Klasse nie falsch benutzt wird, dann kann ein Basisklassendestruktor vielleicht auch mal öffentlich und nicht virtuell sein.



  • Nexus: Deine Funktionalitätsklassen nennt man landläufig Mixins. Unter C++ sind mir diese nie begegnet, aber Ruby hat Sprachunterstützung dafür, weshalb ich sie vor ein paar Monaten kennen gelernt habe. Über die Vor- und Nachteile dieser Technik, wenn man sie über mehrfache Vererbung implementiert, habe ich mir aber noch keine Gedanken gemacht.



  • brotbernd schrieb:

    So verwendet dein TriggeredTimer das Zeugs von Timer indem er davon erbt, was weniger Schreibarbeit bedeutet, als an eine Timer Instanz zu delegieren. Das ist zwar praktisch, birgt aber die Gefahr, dass jemand auf die Idee kommen könnte diese Klassen polymorph zu verwenden.

    Es geht nicht nur darum, Schreib- und Wartungsarbeit zu verringern. Durch die Vererbung kann man TriggeringTimer als Timer ansprechen. Dieses Subtyping funktioniert mit Komposition nicht.

    Ich sehe das Problem nicht ganz. Sowas ist doch genau das klassische Beispiel von Vererbung, das man jedem Anfänger beibringt (der noch nichts von Polymorphie weiss). Natürlich kann man die Objekte aus Versehen polymorph zerstören, aber dann ist man selbst schuld. Man muss ja nicht davon ausgehen, dass vererbte Klassen immer Polymorphie nutzen. 😉

    Michael E. schrieb:

    Nexus: Deine Funktionalitätsklassen nennt man landläufig Mixins.

    Über diesen Begriff bin ich auch vor Kurzem gestolpert. Dass es sich dabei genau um das handelt, war mir jedoch nicht bekannt.


Log in to reply