Wann sollte ich in C ++ final in der Deklaration virtueller Methoden verwenden?

11

Ich weiß, dass das finalSchlüsselwort verwendet wird, um zu verhindern, dass virtuelle Methoden von abgeleiteten Klassen überschrieben werden. Ich kann jedoch kein nützliches Beispiel finden, wenn ich wirklich ein finalSchlüsselwort mit virtualMethode verwenden sollte. Darüber hinaus scheint die Verwendung finalmit virtuellen Methoden ein schlechter Geruch zu sein, da es Programmierern nicht erlaubt, den Unterricht in Zukunft zu erweitern.

Meine Frage ist als nächstes:

Gibt es einen nützlichen Fall, den ich wirklich finalin der virtualMethodendeklaration verwenden sollte?

Metamaker
quelle
Aus den gleichen Gründen, aus denen Java-Methoden endgültig sind?
Ordous
In Java sind alle Methoden virtuell. Die endgültige Methode in der Basisklasse kann die Leistung verbessern. C ++ verfügt über nicht virtuelle Methoden, daher ist dies für diese Sprache nicht der Fall. Kennen Sie andere gute Fälle? Ich würde sie gerne hören. :)
Metamaker
Ich schätze, ich kann die Dinge nicht sehr gut erklären - ich habe versucht, genau dasselbe zu sagen wie Mike Nakis.
Ordous

Antworten:

12

Eine Zusammenfassung dessen, was das finalSchlüsselwort bewirkt: Nehmen wir an, wir haben eine Basisklasse Aund eine abgeleitete Klasse B. Die Funktion f()kann als deklariert Awerden virtual, was bedeutet, dass die Klasse Bsie überschreiben kann. Aber dann Bkönnte die Klasse wünschen, dass jede Klasse, von der weiter abgeleitet wird, Bnicht überschrieben werden kann f(). Das ist , wenn wir erklären müssen , f()wie finalin B. Ohne das finalSchlüsselwort kann jede abgeleitete Klasse eine Methode überschreiben, sobald wir sie als virtuell definieren. Das finalSchlüsselwort wird verwendet, um dieser Freiheit ein Ende zu setzen.

Ein Beispiel dafür , warum Sie so etwas brauchen würde: Angenommen Klasse Adefiniert eine virtuelle Funktion prepareEnvelope()und Klasse Büberschreibt sie und setzt sie als Folge von Aufrufen zu seinen eigenen virtuellen Methoden stuffEnvelope(), lickEnvelope()und sealEnvelope(). Die Klasse Bbeabsichtigt, abgeleiteten Klassen das Überschreiben dieser virtuellen Methoden zu ermöglichen, um ihre eigenen Implementierungen bereitzustellen. Die Klasse Bmöchte jedoch nicht zulassen, dass abgeleitete Klassen überschreiben prepareEnvelope()und somit die Reihenfolge der Inhalte ändern, lecken, versiegeln oder das Aufrufen einer dieser Methoden unterlassen. In diesem Fall wird die Klasse als endgültig Bdeklariert prepareEnvelope().

Mike Nakis
quelle
Zunächst einmal vielen Dank für die Antwort, aber ist das nicht genau das, was ich in einem ersten Satz meiner Frage geschrieben habe? Was ich wirklich brauche, ist ein Fall wann class B might wish that any class which is further derived from B should not be able to override f()? Gibt es einen realen Fall, in dem eine solche Klasse B und Methode f () existiert und gut passt?
Metamaker
Ja, es tut mir leid, Moment mal, ich schreibe gerade ein Beispiel.
Mike Nakis
@metamaker los geht's.
Mike Nakis
1
Wirklich gutes Beispiel, dies ist die bisher beste Antwort. Danke!
Metamaker
1
Dann danke für eine verschwendete Zeit. Ich konnte im Internet kein gutes Beispiel finden und habe viel Zeit mit der Suche verschwendet. Ich würde +1 für die Antwort setzen, aber ich kann es nicht tun, da ich einen schlechten Ruf habe. :( Vielleicht später.
Metamaker
2

Aus gestalterischer Sicht ist es oft nützlich, Dinge als unveränderlich markieren zu können. Auf die gleiche Weise constbietet der Compiler Schutz und zeigt an, dass sich ein Status nicht ändern sollte. Er finalkann verwendet werden, um anzuzeigen, dass sich das Verhalten in der Vererbungshierarchie nicht weiter ändern sollte.

Beispiel

Stellen Sie sich ein Videospiel vor, bei dem Fahrzeuge den Spieler von einem Ort zum anderen bringen. Alle Fahrzeuge sollten vor dem Abflug überprüfen, ob sie zu einem gültigen Ort fahren (z. B. um sicherzustellen, dass die Basis am Ort nicht zerstört wird). Wir können zunächst die nicht-virtuelle Schnittstellensprache (NVI) verwenden, um sicherzustellen, dass diese Prüfung unabhängig vom Fahrzeug durchgeführt wird.

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

Nehmen wir jetzt an, wir haben fliegende Fahrzeuge in unserem Spiel, und alle fliegenden Fahrzeuge benötigen und haben gemeinsam, dass sie vor dem Start eine Sicherheitsüberprüfung im Hangar durchlaufen müssen.

Hier können wir finalsicherstellen, dass alle fliegenden Fahrzeuge eine solche Inspektion durchlaufen, und auch diese Konstruktionsanforderungen von fliegenden Fahrzeugen kommunizieren.

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

Auf finaldiese Weise erweitern wir effektiv die Flexibilität der nicht-virtuellen Schnittstellensprache, um ein einheitliches Verhalten entlang der Vererbungshierarchie (auch nachträglich, um dem fragilen Basisklassenproblem entgegenzuwirken) auf die virtuellen Funktionen selbst bereitzustellen. Darüber hinaus kaufen wir uns Spielraum, um nachträglich zentrale Änderungen vorzunehmen, die sich auf alle Flugfahrzeugtypen auswirken, ohne jede vorhandene Flugfahrzeugimplementierung zu ändern.

Dies ist ein Beispiel für die Verwendung final. Es gibt Kontexte, in denen es einfach nicht sinnvoll ist, eine virtuelle Elementfunktion weiter zu überschreiben. Dies kann zu einem spröden Design und einer Verletzung Ihrer Designanforderungen führen.

Hier finalist es aus gestalterischer / architektonischer Sicht nützlich.

Dies ist auch aus Sicht des Optimierers nützlich, da es dem Optimierer diese Entwurfsinformationen zur Verfügung stellt, die es ihm ermöglichen, virtuelle Funktionsaufrufe zu devirtualisieren (wodurch der dynamische Versandaufwand beseitigt wird und häufig eine Optimierungsbarriere zwischen Anrufer und Angerufenen beseitigt wird).

Frage

Aus den Kommentaren:

Warum sollten final und virtual jemals gleichzeitig verwendet werden?

Es ist nicht sinnvoll, dass eine Basisklasse an der Wurzel einer Hierarchie eine Funktion als beide virtualund deklariert final. Das kommt mir ziemlich albern vor, da sowohl der Compiler als auch der menschliche Leser durch unnötige Reifen springen müssten, was vermieden werden kann, indem virtualin einem solchen Fall einfach alles vermieden wird. Unterklassen erben jedoch Funktionen für virtuelle Elemente wie folgt:

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

In diesem Fall ist es eine virtuelle Funktion , ob Bar::fdas virtuelle Schlüsselwort explizit verwendet wird oder nicht Bar::f. Das virtualSchlüsselwort wird dann in diesem Fall optional. Daher kann es sinnvoll sein Bar::f, als angegeben zu werden final, obwohl es sich um eine virtuelle Funktion handelt ( finalkann nur für virtuelle Funktionen verwendet werden).

Und manche Leute mögen es stilistisch vorziehen, explizit darauf hinzuweisen, dass Bar::fes virtuell ist, wie folgt:

struct Bar: Foo
{
   virtual void f() final {...}
};

Für mich ist es irgendwie überflüssig, in diesem Zusammenhang sowohl virtualund als auch finalBezeichner für dieselbe Funktion zu verwenden (ebenfalls virtualund override), aber in diesem Fall ist es eine Frage des Stils. Einige Leute finden möglicherweise, dass virtualhier etwas Wertvolles kommuniziert, ähnlich wie bei der Verwendung externfür Funktionsdeklarationen mit externer Verknüpfung (obwohl es optional keine anderen Verknüpfungsqualifizierer gibt).


quelle
Wie wollen Sie die privateMethode in Ihrer VehicleKlasse überschreiben ? Meinten Sie nicht protectedstattdessen?
Andy
3
@DavidPacker erstreckt privatesich nicht auf das Überschreiben (etwas kontraintuitiv). Öffentliche / geschützte / private Bezeichner für virtuelle Funktionen gelten nur für Anrufer und nicht für Overrider. Eine abgeleitete Klasse kann virtuelle Funktionen aus ihrer Basisklasse unabhängig von ihrer Sichtbarkeit überschreiben.
@DavidPacker protectedkönnte etwas intuitiver sein. Ich greife einfach lieber nach der geringstmöglichen Sichtbarkeit, wann immer ich kann. Ich denke, der Grund, warum die Sprache so gestaltet ist, ist, dass ansonsten private virtuelle Mitgliedsfunktionen außerhalb des Kontextes der Freundschaft überhaupt keinen Sinn ergeben würden, da keine Klasse außer einem Freund sie dann überschreiben könnte, wenn Zugriffsspezifizierer im Kontext von Bedeutung wären überschreiben und nicht nur anrufen.
Ich dachte, es auf privat zu setzen würde nicht einmal kompilieren. Zum Beispiel erlauben weder Java noch C # dies. C ++ überrascht mich jeden Tag. Auch nach 10 Jahren Programmierung.
Andy
@ DavidPacker Es hat mich auch gestolpert, als ich es zum ersten Mal sah. Vielleicht sollte ich es einfach schaffen protected, um andere nicht zu verwirren. Am Ende habe ich zumindest einen Kommentar abgegeben, der beschreibt, wie private virtuelle Funktionen immer noch überschrieben werden können.
0
  1. Es ermöglicht viele Optimierungen, da zur Kompilierungszeit möglicherweise bekannt ist, welche Funktion aufgerufen wird.

  2. Werfen Sie das Wort "Codegeruch" vorsichtig herum. "final" macht es nicht unmöglich, die Klasse zu erweitern. Doppelklicken Sie auf das "letzte" Wort, drücken Sie die Rücktaste und erweitern Sie die Klasse. JEDOCH final ist eine hervorragende Dokumentation, von der der Entwickler nicht erwartet, dass Sie die Funktion überschreiben, und aus diesem Grund sollte der nächste Entwickler sehr vorsichtig sein, da die Klasse möglicherweise nicht mehr richtig funktioniert, wenn die endgültige Methode auf diese Weise überschrieben wird.

gnasher729
quelle
Warum sollte finalund wird es virtualgleichzeitig verwendet?
Robert Harvey
1
Es ermöglicht viele Optimierungen, da zur Kompilierungszeit möglicherweise bekannt ist, welche Funktion aufgerufen wird. Können Sie erklären, wie oder eine Referenz angeben? JEDOCH final ist eine hervorragende Dokumentation, von der der Entwickler nicht erwartet, dass Sie die Funktion überschreiben, und aus diesem Grund sollte der nächste Entwickler sehr vorsichtig sein, da die Klasse möglicherweise nicht mehr richtig funktioniert, wenn die endgültige Methode auf diese Weise überschrieben wird. Ist das nicht ein schlechter Geruch?
Metamaker
@ RobertHarvey ABI Stabilität. In jedem anderen Fall sollte es wahrscheinlich finalund sein override.
Deduplikator
@metamaker Ich hatte das gleiche Q, das in den Kommentaren hier besprochen wurde - programmers.stackexchange.com/questions/256778/… - und bin lustigerweise in mehrere andere Diskussionen gestolpert, erst seit ich gefragt habe! Grundsätzlich geht es darum, ob ein optimierender Compiler / Linker bestimmen kann, ob eine Referenz / ein Zeiger eine abgeleitete Klasse darstellt und daher einen virtuellen Aufruf benötigt. Wenn die Methode (oder Klasse) nachweislich nicht über den statischen Typ der Referenz hinaus überschrieben werden kann, können sie devirtualisieren: direkt aufrufen. Wenn es irgendwelche Zweifel - wie oft - sie müssen praktisch nennen. Das Deklarieren finalkann ihnen wirklich helfen
underscore_d