Was ist der Zweck des Schlüsselworts "final" in C ++ 11 für Funktionen?

142

Was ist der Zweck des finalSchlüsselworts in C ++ 11 für Funktionen? Ich verstehe, dass es das Überschreiben von Funktionen durch abgeleitete Klassen verhindert, aber wenn dies der Fall ist, reicht es dann nicht aus, Ihre finalFunktionen als nicht virtuell zu deklarieren ? Fehlt mir hier noch etwas?

Lezebulon
quelle
30
" Ist es nicht genug, Ihre" endgültigen "Funktionen als nicht virtuell zu deklarieren? " Nein, überschreibende Funktionen sind implizit virtuell, unabhängig davon, ob Sie das virtualSchlüsselwort verwenden oder nicht.
ildjarn
13
@ildjarn das ist nicht wahr, wenn sie nicht als virtuell in der Superklasse deklariert wurden, können Sie nicht von einer Klasse ableiten und eine nicht virtuelle Methode in eine virtuelle umwandeln ..
Dan O
10
@DanO Ich denke, Sie können nicht überschreiben, aber Sie können eine Methode auf diese Weise "verstecken". Dies führt zu vielen Problemen, da die Leute nicht vorhaben, Methoden zu verstecken.
Alex Kremer
16
@DanO: Wenn es in der Superklasse nicht virtuell ist, würde es nicht "überschreiben".
ildjarn
2
Auch hier hat " Überschreiben " eine spezifische Bedeutung, nämlich einer virtuellen Funktion polymorphes Verhalten zu verleihen. In Ihrem Beispiel funcist es nicht virtuell, es gibt also nichts zu überschreiben und somit nichts als overrideoder zu markieren final.
ildjarn

Antworten:

129

Was Sie vermissen, wie idljarn bereits in einem Kommentar erwähnt hat, ist, dass Sie eine Funktion aus einer Basisklasse möglicherweise nicht als nicht virtuell markieren können , wenn Sie sie überschreiben :

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};
David Rodríguez - Dribeas
quelle
Vielen Dank! Dies ist der Punkt, den ich vermisst habe: dh dass sogar Ihre "Blatt" -Klassen ihre Funktion als virtuell markieren müssen, selbst wenn sie Funktionen überschreiben wollen und nicht selbst überschrieben werden sollen
Lezebulon
8
@lezebulon: Ihre Blattklassen müssen eine Funktion nicht als virtuell markieren, wenn die Superklasse sie als virtuell deklariert hat.
Dan O
5
Die Methoden in den Blattklassen sind implizit virtuell, wenn sie in der Basisklasse virtuell sind. Ich denke, dass Compiler warnen sollten, wenn dieses implizite 'virtuelle' fehlt.
Aaron McDaid
@AaronMcDaid: Compiler warnen normalerweise vor Code, der, wenn er korrekt ist, Verwirrung oder Fehler verursachen kann. Ich habe noch nie jemanden gesehen, der von dieser besonderen Funktion der Sprache auf eine Weise überrascht wurde, die Probleme verursachen könnte, daher weiß ich nicht wirklich, wie nützlich dieser Fehler sein könnte. Im Gegenteil, das Vergessen des virtualkann Fehler verursachen, und C ++ 11 fügte das overrideTag einer Funktion hinzu, die diese Situation erkennt und nicht kompiliert werden kann, wenn sich eine Funktion, die überschreiben soll, tatsächlich verbirgt
David Rodríguez - dribeas
1
In den Änderungshinweisen zu GCC 4.9 heißt es: "Neues Modul zur Analyse der Typvererbung zur Verbesserung der Devirtualisierung. Bei der Devirtualisierung werden jetzt anonyme Namensräume und das endgültige C ++ 11-Schlüsselwort berücksichtigt". Es handelt sich also nicht nur um syntaktischen Zucker, sondern auch um einen potenziellen Optimierungsvorteil.
kfsone
126
  • Es soll verhindern, dass eine Klasse vererbt wird. Aus Wikipedia :

    C ++ 11 bietet außerdem die Möglichkeit, das Erben von Klassen oder das Überschreiben von Methoden in abgeleiteten Klassen zu verhindern. Dies erfolgt mit der speziellen Kennung final. Beispielsweise:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • Es wird auch verwendet, um eine virtuelle Funktion zu markieren, um zu verhindern, dass sie in den abgeleiteten Klassen überschrieben wird:

    struct Base2 {
        virtual void f() final;
    };
    
    struct Derived2 : Base2 {
        void f(); // ill-formed because the virtual function Base2::f has 
                  // been marked final
    };

Wikipedia macht weiter einen interessanten Punkt :

Beachten Sie, dass weder Sprachschlüsselwörter overridenoch finalSprachschlüsselwörter vorhanden sind. Sie sind technisch identifizierend; Sie erhalten nur dann eine besondere Bedeutung, wenn sie in diesen spezifischen Kontexten verwendet werden . An jedem anderen Ort können sie gültige Bezeichner sein.

Das heißt, Folgendes ist zulässig:

int const final = 0;     // ok
int const override = 1;  // ok
Nawaz
quelle
1
danke, aber ich habe vergessen zu erwähnen, dass meine Frage die Verwendung von "final" mit Methoden
betraf
Sie haben es @lezebulon erwähnt :-) "Was ist der Zweck des" endgültigen "Schlüsselworts in C ++ 11 für Funktionen ". (meine Betonung)
Aaron McDaid
Du hast es bearbeitet? Ich sehe keine Nachricht mit der Aufschrift "Vor x Minuten von Lezebulon bearbeitet". Wie ist das passiert? Vielleicht haben Sie es nach dem Absenden sehr schnell bearbeitet?
Aaron McDaid
5
@Aaron: Änderungen, die innerhalb von fünf Minuten nach dem Posten vorgenommen wurden, werden im Revisionsverlauf nicht berücksichtigt.
ildjarn
@Nawaz: Warum sind sie keine Schlüsselwörter, sondern nur Spezifizierer? Ist es aus Kompatibilitätsgründen möglich, dass bereits vorhandener Code vor C ++ 11 final & override für andere Zwecke verwendet?
Zerstörer
45

"final" ermöglicht es einer Compileroptimierung auch, den indirekten Aufruf zu umgehen:

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

Mit "final" kann der Compiler CDerived::DoSomething()direkt von innen Blah()oder sogar inline aufrufen . Ohne sie muss ein indirekter Aufruf innerhalb von generiert werden, Blah()da Blah()er innerhalb einer abgeleiteten Klasse aufgerufen werden kann, die überschrieben wurde DoSomething().

Chris Green
quelle
29

Den semantischen Aspekten von "final" gibt es nichts hinzuzufügen.

Aber ich möchte zu Chris Green's Kommentar hinzufügen, dass "final" in nicht allzu ferner Zukunft eine sehr wichtige Compiler-Optimierungstechnik werden könnte. Nicht nur in dem von ihm erwähnten einfachen Fall, sondern auch für komplexere reale Klassenhierarchien, die durch "final" "geschlossen" werden können, wodurch Compiler einen effizienteren Dispatching-Code generieren können als mit dem üblichen vtable-Ansatz.

Ein wesentlicher Nachteil von vtables besteht darin, dass für ein solches virtuelles Objekt (unter der Annahme von 64 Bit auf einer typischen Intel-CPU) der Zeiger allein 25% (8 von 64 Byte) einer Cache-Zeile verbraucht. Bei den Bewerbungen, die ich gerne schreibe, tut das sehr weh. (Und meiner Erfahrung nach ist es aus puristischer Sicht das wichtigste Argument gegen C ++, dh von C-Programmierern.)

In Anwendungen, die extreme Leistung erfordern, was für C ++ nicht so ungewöhnlich ist, kann dies in der Tat fantastisch werden, da dieses Problem nicht manuell im C-Stil oder durch seltsames Vorlagen-Jonglieren umgangen werden muss.

Diese Technik ist als Devirtualisierung bekannt . Ein Begriff, an den man sich erinnern sollte. :-)

Es gibt eine großartige Rede von Andrei Alexandrescu, die ziemlich gut erklärt, wie Sie solche Situationen heute umgehen können und wie "final" dazu beitragen könnte, ähnliche Fälle "automatisch" in Zukunft zu lösen (mit den Zuhörern besprochen):

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly

Mario Knezović
quelle
23
8 ist 25% von 64?
ildjarn
6
Kennt jemand einen Compiler, der diese jetzt nutzt?
Vincent Fourmond
das gleiche, was ich sagen möchte.
Crazii
8

Final kann nicht auf nicht virtuelle Funktionen angewendet werden.

error: only virtual member functions can be marked 'final'

Es wäre nicht sehr sinnvoll, eine nicht virtuelle Methode als "endgültig" markieren zu können. Gegeben

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()wird immer anrufen A::foo.

Aber wenn A virtual:: foo wäre, würde B :: foo es überschreiben. Dies könnte unerwünscht sein, und daher wäre es sinnvoll, die virtuelle Funktion endgültig zu machen.

Die Frage ist jedoch, warum endgültige virtuelle Funktionen zulässig sind. Wenn Sie eine tiefe Hierarchie haben:

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

Dann finallegt das einen "Boden" fest, wie viel Überschreiben getan werden kann. Andere Klassen können A und B erweitern und ihre überschreiben foo, aber wenn eine Klasse C erweitert, ist dies nicht zulässig.

Es macht also wahrscheinlich keinen Sinn, das "Top-Level" -Foo zu machen final, aber es könnte weiter unten Sinn machen.

(Ich denke jedoch, dass es Raum gibt, die Wörter final zu erweitern und auf nicht virtuelle Mitglieder zu überschreiben. Sie hätten jedoch eine andere Bedeutung.)

Aaron McDaid
quelle
Danke für das Beispiel, da war ich mir nicht sicher. Aber dennoch: Was bringt es, eine endgültige (und virtuelle) Funktion zu haben? Grundsätzlich könnten Sie niemals die Tatsache nutzen, dass die Funktion virtuell ist, da sie nicht überschrieben werden kann
Lezebulon
@lezebulon, ich habe meine Frage bearbeitet. Aber dann bemerkte ich die Antwort von DanO - es ist eine gute klare Antwort auf das, was ich sagen wollte.
Aaron McDaid
Ich bin kein Experte, aber ich denke, dass es manchmal sinnvoll sein kann, eine Top-Level-Funktion zu erstellen final. Wenn Sie beispielsweise wissen, dass Sie alle Shapes möchten - etwas foo()Vordefiniertes und Bestimmtes, das keine abgeleitete Form ändern sollte. Oder irre ich mich und es gibt ein besseres Muster für diesen Fall? EDIT: Oh, vielleicht, weil man in diesem Fall einfach nicht die oberste Ebene erreichen foo() virtualsollte? Trotzdem kann es ausgeblendet werden, auch wenn es korrekt (polymorph) über Shape*...
Andrew Cheong
8

Ein Anwendungsfall für das Schlüsselwort 'final', das mir gefällt, lautet wie folgt:

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};
YoungJohn
quelle
1
Ja, dies ist im Wesentlichen ein Beispiel für das Muster der Vorlagenmethode. Und vor C ++ 11 war es immer das TMP, das mich wünschte, C ++ hätte eine Sprachfunktion wie "final", wie es Java tat.
Kaitain
6

final Fügt eine explizite Absicht hinzu, Ihre Funktion nicht überschreiben zu lassen, und verursacht einen Compilerfehler, falls dies verletzt wird:

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

In der jetzigen Form wird der Code kompiliert und B::fooüberschrieben A::foo. B::fooist übrigens auch virtuell. Wenn wir jedoch # 1 in ändern virtual int foo() final, ist dies ein Compilerfehler, und wir dürfen A::fooin abgeleiteten Klassen nicht weiter überschreiben .

Beachten Sie, dass wir dadurch eine neue Hierarchie nicht "wieder öffnen" können, dh es gibt keine Möglichkeit, B::fooeine neue, nicht verwandte Funktion zu erstellen, die unabhängig an der Spitze einer neuen virtuellen Hierarchie stehen kann. Sobald eine Funktion endgültig ist, kann sie in keiner abgeleiteten Klasse mehr deklariert werden.

Kerrek SB
quelle
5

Mit dem letzten Schlüsselwort können Sie eine virtuelle Methode deklarieren, N-mal überschreiben und dann festlegen, dass dies nicht mehr überschrieben werden kann. Es wäre nützlich, um die Verwendung Ihrer abgeleiteten Klasse einzuschränken, damit Sie sagen können: "Ich weiß, dass Sie mit meiner Superklasse dies überschreiben können, aber wenn Sie von mir ableiten möchten, können Sie das nicht!".

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

Wie andere Poster betonten, kann es nicht auf nicht virtuelle Funktionen angewendet werden.

Ein Zweck des endgültigen Schlüsselworts besteht darin, ein versehentliches Überschreiben einer Methode zu verhindern. In meinem Beispiel war DoStuff () möglicherweise eine Hilfsfunktion, die die abgeleitete Klasse einfach umbenennen muss, um ein korrektes Verhalten zu erzielen. Ohne final würde der Fehler erst beim Testen entdeckt.

Dan O.
quelle
1

Das letzte Schlüsselwort in C ++ verhindert beim Hinzufügen zu einer Funktion, dass diese von einer Basisklasse überschrieben wird. Auch beim Hinzufügen zu einer Klasse wird die Vererbung jeglicher Art verhindert. Betrachten Sie das folgende Beispiel, das die Verwendung des endgültigen Spezifizierers zeigt. Dieses Programm schlägt bei der Kompilierung fehl.

#include <iostream>
using namespace std;

class Base
{
  public:
  virtual void myfun() final
  {
    cout << "myfun() in Base";
  }
};
class Derived : public Base
{
  void myfun()
  {
    cout << "myfun() in Derived\n";
  }
};

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

Ebenfalls:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}
Krishna Ganeriwal
quelle
0

Ergänzung zu Mario Knezovićs Antwort:

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

Der obige Code zeigt die Theorie, wurde jedoch nicht auf echten Compilern getestet. Sehr geschätzt, wenn jemand eine zerlegte Ausgabe einfügt.

crazii
quelle