Aus gestalterischer Sicht ist es oft nützlich, Dinge als unveränderlich markieren zu können. Auf die gleiche Weise const
bietet der Compiler Schutz und zeigt an, dass sich ein Status nicht ändern sollte. Er final
kann 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 final
sicherstellen, 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 final
diese 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 final
ist 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 virtual
und 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 virtual
in 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::f
das virtuelle Schlüsselwort explizit verwendet wird oder nicht Bar::f
. Das virtual
Schlü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 ( final
kann nur für virtuelle Funktionen verwendet werden).
Und manche Leute mögen es stilistisch vorziehen, explizit darauf hinzuweisen, dass Bar::f
es virtuell ist, wie folgt:
struct Bar: Foo
{
virtual void f() final {...}
};
Für mich ist es irgendwie überflüssig, in diesem Zusammenhang sowohl virtual
und als auch final
Bezeichner für dieselbe Funktion zu verwenden (ebenfalls virtual
und override
), aber in diesem Fall ist es eine Frage des Stils. Einige Leute finden möglicherweise, dass virtual
hier etwas Wertvolles kommuniziert, ähnlich wie bei der Verwendung extern
für Funktionsdeklarationen mit externer Verknüpfung (obwohl es optional keine anderen Verknüpfungsqualifizierer gibt).