Sinn von final und override in C++11



  • Hallo,

    Wieso lassen sich final und override eigentlich nur mit virtuellen Funktionen benutzten. Bei override mag das ja noch seinen Sinn haben, aber bei final?
    Spielen virtuelle Funktionen in C++ eigentlich noch eine Rolle. Die kann man doch mit Templates rauskürzen?



  • Gasthof schrieb:

    Wieso lassen sich final und override eigentlich nur mit virtuellen Funktionen benutzten. Bei override mag das ja noch seinen Sinn haben, aber bei final?

    Welchen Sinn sollte es denn haben?

    Gasthof schrieb:

    Spielen virtuelle Funktionen in C++ eigentlich noch eine Rolle. Die kann man doch mit Templates rauskürzen?

    Noe, kann man nicht.



  • Hallo,

    Gasthof schrieb:
    Wieso lassen sich final und override eigentlich nur mit virtuellen Funktionen benutzten. Bei override mag das ja noch seinen Sinn haben, aber bei final?

    Welchen Sinn sollte es denn haben?

    Ich meinte folgendes:

    struct A {
      void foo() final {/*...*/ };
    };
    

    Damit sichergestellt, dass man foo nicht mehr überladen kann.
    Virtuelle Funktionen sind aber doch dafür da, dass man eine Funktion in einer abgeleiteten Klasse überschreiben kann. Darum sehe ich keinen Sinn darin eine virtuelle Funktion final zu machen aber sehr wohl eine nicht virtuelle Funktion.

    Gasthof schrieb:
    Spielen virtuelle Funktionen in C++ eigentlich noch eine Rolle. Die kann man doch mit Templates rauskürzen?

    Noe, kann man nicht.

    Ich meinte Folgendes:

    template<class D>
    struct A {
      void foo() const { static_cast<const D*>(this); }
    };
    
    struct B : public A<B> {
      void foo() const { /*...*/ };
    };
    


  • Gasthof schrieb:

    Spielen virtuelle Funktionen in C++ eigentlich noch eine Rolle. Die kann man doch mit Templates rauskürzen?

    Typischer Anfängerfehler, weil dauernd von statischer polymorphie gesprochen wird.



  • Gasthof schrieb:

    Ich meinte folgendes:

    struct A {
      void foo() final {/*...*/ };
    };
    

    Damit sichergestellt, dass man foo nicht mehr überladen kann.
    Virtuelle Funktionen sind aber doch dafür da, dass man eine Funktion in einer abgeleiteten Klasse überschreiben kann. Darum sehe ich keinen Sinn darin eine virtuelle Funktion final zu machen aber sehr wohl eine nicht virtuelle Funktion.

    Die Klasse hat keinen virtuellen dtor, also ist von ihr ableiten schonmal grundsaetzlich eine schlechte Idee.

    Eine virtuelle Funktion macht man final, weil man nicht will, dass sie in weiteren, abgeleiteten Klassen ueberschrieben wird. Diese Information kann der Compiler auch zur Optimierung benutzen.

    Gasthof schrieb:

    Ich meinte Folgendes:

    template<class D>
    struct A {
      void foo() const { static_cast<const D*>(this); }
    };
    
    struct B : public A<B> {
      void foo() const { /*...*/ };
    };
    

    Und jetzt versuch mal verschiedene A's in einen Container zu stecken und foo() auf ihnen aufzurufen.



  • Die Klasse hat keinen virtuellen dtor, also ist von ihr ableiten schonmal grundsaetzlich eine schlechte Idee.

    Nicht unbedingt. Ich darf A halt nicht als Basisklasse verwenden.

    Eine virtuelle Funktion macht man final, weil man nicht will, dass sie in weiteren, abgeleiteten Klassen ueberschrieben wird. Diese Information kann der Compiler auch zur Optimierung benutzen.

    Sicher. Aber warum brauch brauch ich dafür eine virtuelle Funktion. Eine Einfache würde es doch auch tun und der overhead fällt weg.

    quote="Gasthof"]Ich meinte Folgendes:

    template<class D>
    struct A {
      void foo() const { static_cast<const D*>(this)->foo(); }
    };
    
    struct B : public A<B> {
      void foo() const { /*...*/ };
    };
    

    Und jetzt versuch mal verschiedene A's in einen Container zu stecken und foo() auf ihnen aufzurufen.

    auto x = make_tuple<A<B>, A<C>, A<D> /*, ...*/>(B(), C(), D());
    get<0>(x).foo();
    get<1>(x).foo();
    get<2>(x).foo();
    /*...*/
    


  • Gasthof schrieb:

    Nicht unbedingt. Ich darf A halt nicht als Basisklasse verwenden.

    Das sagte ich doch gerade?!

    Gasthof schrieb:

    Sicher. Aber warum brauch brauch ich dafür eine virtuelle Funktion. Eine Einfache würde es doch auch tun und der overhead fällt weg.

    struct foo
    {
        virtual void f() = 0;
    };
    
    struct bar : foo
    {
        virtual void f() override final;
    };
    
    struct baz : bar
    {
        virtual void f() override; // ups, error
    };
    

    Gasthof schrieb:

    [...]

    Die Rede ist selbstverstaendlich von STL-Containern.



  • Gasthof schrieb:

    Ich meinte folgendes:

    struct A {
      void foo() final {/*...*/ };
    };
    

    Damit sichergestellt, dass man foo nicht mehr überladen kann.

    Du scheinst hier zwei verschiedene Dinge in einen Topf zu werden:
    - Überladen (overloading)
    - Überschreiben (overriding)

    Gasthof schrieb:

    Virtuelle Funktionen sind aber doch dafür da, dass man eine Funktion in einer abgeleiteten Klasse überschreiben kann. Darum sehe ich keinen Sinn darin eine virtuelle Funktion final zu machen aber sehr wohl eine nicht virtuelle Funktion.

    final für nicht-virtuelle Funktionen ergibt keinen Sinn, weil man sie sowieso nicht überschreiben kann. final für virtuelle Funktionen kannst du benutzen, um in einer abgeleiteten Klasse zu sagen, dass eine virtuelle Funktion nicht nochmal überschrieben werden soll.

    Gasthof schrieb:

    Spielen virtuelle Funktionen in C++ eigentlich noch eine Rolle.

    Klar.



  • final für nicht-virtuelle Funktionen ergibt keinen Sinn, weil man sie sowieso nicht überschreiben kann.

    Doch kann man.

    class MyVector : public std::vector<int> {
    public:
    //...
      int operator[](int i) { return this->at(i); }
    };
    

    Nun kann man MyVector mit Längenüberprüfung nutzen.
    Stand so oder so ähnlich mal im Stroustrup drin.

    Man darf nun halt keine Elemente initialisieren, die Aufräumarbeiten fordern und/oder std::vector<int> nicht als Basisklasse nehmen.



  • Gasthof schrieb:

    Doch kann man.

    Nein, du verdeckst nur. Man kann immernoch das original aufrufen.

    Es ist das selbe wie:

    int foo() {
       int var=3;
       {
          int var;
          var =4;
          cout<<var; //4
       }
       cout<<var; //3
    }
    


  • Gasthof schrieb:

    final für nicht-virtuelle Funktionen ergibt keinen Sinn, weil man sie sowieso nicht überschreiben kann.

    Doch kann man.

    class MyVector : public std::vector<int> {
    public:
    //...
      int operator[](int i) { return this->at(i); }
    };
    

    Nun kann man MyVector mit Längenüberprüfung nutzen.
    Stand so oder so ähnlich mal im Stroustrup drin.

    Eher "so ähnlich", da er ganz sicher nicht "overriding" dazu sagte. Du deklarierst einfach eine neue Funktion in einem neuen Scope. Diese Funktion hat mit der Funktion aus vector<int> nichts zu tun. Sie haben nur "zufällig" den gleichen Namen. Das ist kein "overriding". Das ist "hiding", da die neue Funktion die alte nur "verdeckt". Das mit dem Verdecken ergibt sich aus den Regeln des Name Lookups, wobei Scopes immer von innen nach außen durchsucht werden, so lange man noch nichts gefunden hat. Hat man etwas gefunden, bricht dieser Teil der Namenssuche ab. Daher kann ein Name eines inneren Scopes einen Namen aus einem äußeren Scope verdecken. Mit Überschreiben hat das nichts zu tun. Überschreiben beteutet, dass das Verhalten einer virtuellen Funktion geändert bzw überhaupt erstmal definiert wird (falls sie pur-virtuell war). Deswegen macht das Wort im Kontext von nicht-virtuellen Funktionen gar keinen Sinn.

    Aber da du auch nach dem Sinn von "override" fragst. Das ist gerade dazu da, Fehler zu vermeiden. Es gibt Situationen, wo der Programmierer eine virtuelle Funktion überschreiben will, statt einfach eine neue Funktion zu deklarieren. Und mit "override" hat er jetzt die Gelegenheit, genau das auszudrücken.

    class base
    {
    public:
      virtual void dings(int);
      virtual void bums(int);
      virtual void foo(int);
    };
    
    class derived : public base
    {
    public:
      void dings(int);          // alte Funktion wird überschrieben
      void bums(short);         // neue Funktion, die die alte nur verdeckt
      void foo(short) override; // *
    };
    

    Im letzten Fall (*) gibt es eine Diskrepanz zwischen dem, was der Programmierer möchte (overriding) und dem, was der C++ Compiler sonst tun würde. Weil die Parameter für foo unterschiedlich sind, würde hier eine neue Funktion foo entstehen, die die alte nicht überschreibt sondern nur verdeckt. Solche Fälle kann der Compiler jetzt abfangen. Er erkennt hier, dass das, was der Programmierer wollte, nämlich foo überschreiben, nicht mit einer bereits in der Basisklasse existenten virtuellen Funktion zusammenpasst und streicht das als Fehler an.

    Wenn du das bei nicht-virtuellen Funktionen versuchst...

    class base {
    public:
      void foo(int);
    };
    
    class derived : public base {
      void foo(int) override; // Fehler!
    };
    

    wird dir das auch als Fehler angestrichen, weil es in base keine virtuelle, non-const Funktion foo gibt, die ein int entgegen nimmt. Daher kann man da auch nichts überschreiben; denn überschreiben kann man nur virtuelle Funktionen, die noch nicht final sind.



  • Aber da du auch nach dem Sinn von "override" fragst. Das ist gerade dazu da, Fehler zu vermeiden. Es gibt Situationen, wo der Programmierer eine virtuelle Funktion überschreiben will, statt einfach eine neue Funktion zu deklarieren. Und mit "override" hat er jetzt die Gelegenheit, genau das auszudrücken.

    Der Sinn von Override ist mir klar. Es ging mir eher darum, warum ich beides nur mit virtual nutzen kann.

    Eher "so ähnlich", da er ganz sicher nicht "overriding" dazu sagte. Du deklarierst einfach eine neue Funktion in einem neuen Scope. Diese Funktion hat mit der Funktion aus vector<int> nichts zu tun. Sie haben nur "zufällig" den gleichen Namen. Das ist kein "overriding". Das ist "hiding", da die neue Funktion die alte nur "verdeckt". Das mit dem Verdecken ergibt sich aus den Regeln des Name Lookups, wobei Scopes immer von innen nach außen durchsucht werden, so lange man noch nichts gefunden hat. Hat man etwas gefunden, bricht dieser Teil der Namenssuche ab. Daher kann ein Name eines inneren Scopes einen Namen aus einem äußeren Scope verdecken. Mit Überschreiben hat das nichts zu tun. Überschreiben beteutet, dass das Verhalten einer virtuellen Funktion geändert bzw überhaupt erstmal definiert wird (falls sie pur-virtuell war). Deswegen macht das Wort im Kontext von nicht-virtuellen Funktionen gar keinen Sinn.

    Es war mir nicht klar, dass man (vom Sprachgebrauch ) zwischen den beiden unterscheidet.



  • Was hat das mit Sprachgebraucht zu tun?
    Der Standard sagt dass "override" nur die Sache bedeutet die man mit virtuellen Funktionen machen kann, und nicht die Sache die man mit allen Funktionen machen kann.
    Wie man dazu sagt ist vollkommen egal.

    Das ist der Sinn von "override". Dass dir das klar war, kann ich mir irgendwie nicht vorstellen, denn sonst wäre dir auch klar gewesen wieso es mit nicht-virtuellen Funktionen keinen Sinn ergibt.

    Das selbe gilt für final.



  • Das ist der Sinn von "override". Dass dir das klar war, kann ich mir irgendwie nicht vorstellen, denn sonst wäre dir auch klar gewesen wieso es mit nicht-virtuellen Funktionen keinen Sinn ergibt.

    Statische Polymorphie?

    template<class D>
    struct A {
      void foo() const { static_cast<const D*>(this)->foo(); }
    };
    
    struct B : public A<B> {
      void foo() const override { /*...*/ } //Fehler
    };
    


  • Gasthof schrieb:

    final für nicht-virtuelle Funktionen ergibt keinen Sinn, weil man sie sowieso nicht überschreiben kann.

    Doch kann man.

    class MyVector : public std::vector<int> {
    public:
    //...
      int operator[](int i) { return this->at(i); }
    };
    

    Nun kann man MyVector mit Längenüberprüfung nutzen.
    Stand so oder so ähnlich mal im Stroustrup drin.

    Man darf nun halt keine Elemente initialisieren, die Aufräumarbeiten fordern und/oder std::vector<int> nicht als Basisklasse nehmen.

    Erstens würde Stroustrup nicht von std::vector ableiten, da der Destruktor nicht virtuell ist (hast Du selbst angedeutet), zweitens würde er den operator[] mit size_type statt int definieren und drittens würde er ihn auch noch zumindest mal const machen.



  • ich bins schrieb:

    Erstens würde Stroustrup nicht von std::vector ableiten ...

    Interessante Theorie. In meiner Ausgabe des Stroustrup ist dieses Beispiel aber auch.



  • Erstens würde Stroustrup nicht von std::vector ableiten, da der Destruktor nicht virtuell ist (hast Du selbst angedeutet), zweitens würde er den operator[] mit size_type statt int definieren und drittens würde er ihn auch noch zumindest mal const machen.

    Hallo,

    Ich bin mir auch nicht wirklich sicher ob es im Stroustrup drin stand. Kann auch ein anderes Buch gewesen sein. Das das Beispiel nicht genauso aussieht wie bei mir ist auch klar. Ich finde es momentan nicht mehr.
    (Ja ich hab's so behauptet, war aber anders gemeint. 😞 )



  • Ich hab's gefunden:
    Das Beispiel ist auf Seite 53.



  • Gasthof schrieb:

    Ich hab's gefunden:
    Das Beispiel ist auf Seite 53.

    war mir auch sicher - und habs mal rausgesucht

    <a href= schrieb:

    http://www.stroustrup.com/Programming/std_lib_facilities.h">

    // trivially range-checked vector (no iterator checking):
    template< class T> struct Vector : public std::vector<T> {
    	typedef typename std::vector<T>::size_type size_type;
    
    	Vector() { }
    	explicit Vector(size_type n) :std::vector<T>(n) {}
    	Vector(size_type n, const T& v) :std::vector<T>(n,v) {}
    	template <class I>
    	Vector(I first, I last) :std::vector<T>(first,last) {}
    
    	T& operator[](unsigned int i) // rather than return at(i);
    	{
    		if (i<0||this->size()<=i) throw Range_error(i);
    		return std::vector<T>::operator[](i);
    	}
    	const T& operator[](unsigned int i) const
    	{
    		if (i<0||this->size()<=i) throw Range_error(i);
    		return std::vector<T>::operator[](i);
    	}
    };
    

    bb



  • Gasthof schrieb:

    Das ist der Sinn von "override". Dass dir das klar war, kann ich mir irgendwie nicht vorstellen, denn sonst wäre dir auch klar gewesen wieso es mit nicht-virtuellen Funktionen keinen Sinn ergibt.

    Statische Polymorphie?

    template<class D>
    struct A {
      void foo() const { static_cast<const D*>(this)->foo(); }
    };
    
    struct B : public A<B> {
      void foo() const override { /*...*/ } //Fehler
    };
    

    Blub.

    Eine virtuelle Funktion zu überschreiben und eine neue Funktion zu definieren die eine alte überdeckt sind zwei grundverschiedene Dinge.

    Der Sinn von "override" ist es einen ganz bestimmten Fehler beim compilieren zu finden. Nämlich den wo ein Programmierer meint er überschreibt eine Funktion, das in Wirklichkeit aber nicht tut. (Dass man nur virtuelle Funktionen überschreiben kann wurde ja schon erwähnt.)
    Virtuelle Funktionen überschreibt man nämlich nicht ohne Grund, d.h. der "override" wird benötigt. Findet er nicht statt, weil man z.B. nicht exakt die selbe Signatur verwendet hat, oder weil die Funktion in der Basisklasse nicht (mehr) virtual ist, hat man also ein Problem.

    Das ist auch keine akademische Sache, diese Probleme sind ganz real. Änder (*) 'mal eine virtuelle Funktion in einer von vielen Projekten genutzten Basisklasse (u.U. Projekte deren Code du nicht mal hast, weil du bloss eine Open Source Library entwickelst, und nicht mal weisst wer die alles verwendet). Und dann stell sicher dass alle Programme die diese Funktion überschreiben die Änderung mit machen.
    Da es ohne "override" keinen Weg gibt das sicherzustellen ist das ein Ding der unmöglichkeit. Der Fehler wird nicht beim Kompilieren gemeldet, sondern taucht erst irgendwann zur Laufzeit auf. Wenn das ganze nur einen selten genutzten Codepfad betrifft, oder die sich daraus ergebenen Fehler so subtil sind dass man nicht sofort beim 1. Testlauf aufmerksam wird, dann viel Spass beim Fehlersuchen.

    Daher wurde "override" eingeführt, damit man eben sicherstellen kann dass die "Verbindung" zwischen der Funktion in der Basisklasse und der Funktion in der abgeleiteten Klasse nicht aufgegangen ist.
    Sobald die ehemals überschreibende Funktion nichts mehr überschreibt bekommt man einen Fehler beim Kompilieren, und alle sind glücklich.

    D.h. es ist eben gerade der Sinn von "override" nicht ohne virtuelle Funktionen zu funktionieren. Wem also nicht klar ist warum man "override" nur mit virtuellen Funktionen verwenden kann, dem kann auch den Sinn von "override" nicht klar sein.

    *: Und "ändern" einer virtuellen Funktion kann jetzt auch heissen dass man sie von virtual auf nicht-virtual ändert, weil man meint dass das virtual eigentlich eh sinnlos war. Dann existiert immer noch eine Funktion mit einer exakt passenden Signatur in der Basisklasse, aber überschrieben wird nichts mehr, weil ja nicht mehr virtual.


Log in to reply