Często jest użyteczne z punktu widzenia projektowania, aby móc oznaczyć rzeczy jako niezmienne. W ten sam sposób const
zapewnia kompilator chroni i wskazuje, że stan nie powinien się zmienić, final
można użyć do wskazania, że zachowanie nie powinno się zmieniać dalej w dół hierarchii dziedziczenia.
Przykład
Rozważ grę wideo, w której pojazdy zabierają gracza z jednego miejsca do drugiego. Wszystkie pojazdy powinny sprawdzić przed wyjazdem, czy podróżują do ważnej lokalizacji (upewniając się, że baza w tej lokalizacji nie jest zniszczona, np.). Możemy zacząć od użycia nie-wirtualnego idiomu interfejsu (NVI), aby zagwarantować, że ta kontrola zostanie przeprowadzona niezależnie od pojazdu.
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;
};
Załóżmy teraz, że w naszej grze mamy latające pojazdy, a wszystkim , co wymaga i łączy wszystkie latające pojazdy, jest to, że przed startem muszą przejść kontrolę bezpieczeństwa w hangarze.
W tym miejscu możemy final
zagwarantować, że wszystkie latające pojazdy przejdą taką kontrolę, a także poinformować o wymaganiach projektowych dotyczących latających pojazdów.
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;
};
Używając final
w ten sposób, skutecznie rozszerzamy elastyczność nie-wirtualnego idiomu interfejsu, aby zapewnić jednolite zachowanie w hierarchii dziedziczenia (nawet po zastanowieniu się, przeciwdziałając delikatnemu problemowi z klasą bazową) na same funkcje wirtualne. Co więcej, kupujemy sobie wiggle miejsce, aby dokonać centralnych zmian, które wpływają na wszystkie typy latających pojazdów, bez modyfikowania każdej implementacji każdego latającego pojazdu.
To jeden z takich przykładów użycia final
. Istnieją konteksty, w których można się spotkać, w których nie ma sensu dalsze zastępowanie funkcji wirtualnego elementu członkowskiego - może to prowadzić do kruchego projektu i naruszenia wymagań projektowych.
Jest final
to przydatne z punktu widzenia projektowania / architektury.
Jest to również przydatne z punktu widzenia optymalizatora, ponieważ dostarcza optymalizatorowi tych informacji projektowych, które umożliwiają dewiryzację wirtualnych wywołań funkcji (eliminując narzut dynamicznej wysyłki, a często bardziej znacząco, eliminując barierę optymalizacji między dzwoniącym a odbiorcą).
Pytanie
Z komentarzy:
Dlaczego w tym samym czasie miałaby być używana wersja ostateczna i wirtualna?
Nie ma sensu, aby klasa podstawowa w katalogu głównym hierarchii deklarowała funkcję zarówno jako, jak virtual
i final
. Wydaje mi się to dość głupie, ponieważ zmusiłoby to zarówno kompilatora, jak i czytelnika do przeskakiwania niepotrzebnych obręczy, których można uniknąć, po prostu unikając virtual
w takim przypadku wprost. Jednak podklasy dziedziczą takie funkcje elementów wirtualnych:
struct Foo
{
virtual ~Foo() {}
virtual void f() = 0;
};
struct Bar: Foo
{
/*implicitly virtual*/ void f() final {...}
};
W tym przypadku, niezależnie od tego, czy Bar::f
jawnie używa się wirtualnego słowa kluczowego, Bar::f
jest funkcją wirtualną. Słowo virtual
kluczowe staje się wówczas opcjonalne. Dlatego może być sensowne Bar::f
określenie jako final
, nawet jeśli jest to funkcja wirtualna ( final
może być używana tylko w przypadku funkcji wirtualnych).
I niektórzy ludzie wolą, stylistycznie, jawnie wskazywać, że Bar::f
jest wirtualny, tak jak:
struct Bar: Foo
{
virtual void f() final {...}
};
Dla mnie to trochę zbędne stosowanie obu virtual
i final
specyfikatorów dla tej samej funkcji w tym kontekście (podobnie virtual
i override
), ale w tym przypadku jest to kwestia stylu. Niektóre osoby mogą uznać, że virtual
przekazuje tutaj coś cennego, podobnie jak w extern
przypadku deklaracji funkcji z zewnętrznym powiązaniem (chociaż opcjonalnie nie ma innych kwalifikatorów powiązania).