Kiedy w C ++ powinienem używać końcowego w deklaracji metody wirtualnej?

11

Wiem, że finalsłowo kluczowe jest używane, aby zapobiec zastąpieniu metody wirtualnej przez klasy pochodne. Nie mogę jednak znaleźć żadnego przydatnego przykładu, w którym powinienem naprawdę używać finalsłowa kluczowego z virtualmetodą. Co więcej, wydaje się, że korzystanie finalz metod wirtualnych jest nieprzyjemnym zapachem, ponieważ uniemożliwia programistom rozszerzanie klasy w przyszłości.

Moje pytanie jest następne:

Czy jest jakiś użyteczny przypadek, kiedy naprawdę powinienem użyć finalw virtualdeklaracji metody?

metamaker
źródło
Z tych samych powodów, dla których metody Java są ostateczne?
Ordous,
W Javie wszystkie metody są wirtualne. Ostateczna metoda w klasie bazowej może poprawić wydajność. C ++ ma metody inne niż wirtualne, więc nie jest tak w przypadku tego języka. Czy znasz jakieś inne dobre przypadki? Chciałbym je usłyszeć. :)
metamaker,
Chyba nie jestem zbyt dobry w wyjaśnianiu rzeczy - starałem się powiedzieć dokładnie to samo, co Mike Nakis.
Ordous,

Odpowiedzi:

12

Podsumowanie tego, co finalrobi słowo kluczowe: Załóżmy, że mamy klasę podstawową Ai klasę pochodną B. Funkcja f()może być zadeklarowana Ajako as virtual, co oznacza, że ​​klasa Bmoże ją zastąpić. Ale wtedy klasa Bmoże życzyć sobie, aby jakakolwiek klasa, która jest pochodną dalej, Bnie była w stanie zastąpić f(). Właśnie wtedy musimy zadeklarować f()jako finalza B. Bez finalsłowa kluczowego, gdy zdefiniujemy metodę jako wirtualną, dowolna klasa pochodna będzie mogła ją zastąpić. Słowo finalkluczowe służy zakończeniu tej wolności.

Przykładem dlaczego trzeba coś takiego: Załóżmy, że klasa Adefiniuje funkcję wirtualną prepareEnvelope(), a klasa Bzastępuje ją i wdrożył go jako sekwencji połączeń do własnych metod wirtualnych stuffEnvelope(), lickEnvelope()a sealEnvelope(). Klasa Bzamierza zezwolić klasom pochodnym na przesłonięcie tych wirtualnych metod w celu zapewnienia własnych implementacji, ale klasa Bnie chce pozwolić żadnej klasie pochodnej na przesłonięcie, prepareEnvelope()a tym samym zmienić kolejność rzeczy, lizać, uszczelniać lub pomijać wywoływanie jednej z nich. Tak więc w tym przypadku klasa Bdeklaruje prepareEnvelope()jako ostateczną.

Mike Nakis
źródło
Po pierwsze, dziękuję za odpowiedź, ale czy nie jest to dokładnie to, co napisałem w pierwszym zdaniu mojego pytania? To, czego naprawdę potrzebuję, to przypadek, kiedy class B might wish that any class which is further derived from B should not be able to override f()? Czy istnieje przypadek w świecie rzeczywistym, w którym taka klasa B i metoda f () istnieją i dobrze pasują?
metamaker
Tak, przepraszam, poczekaj, piszę teraz przykład.
Mike Nakis,
@metamaker, proszę bardzo.
Mike Nakis,
1
Naprawdę dobry przykład, to najlepsza jak dotąd odpowiedź. Dzięki!
metamaker
1
Więc dziękuję za zmarnowany czas. Nie mogłem znaleźć dobrego przykładu w Internecie, traciłem dużo czasu na wyszukiwanie. Dałbym +1 za odpowiedź, ale nie mogę tego zrobić, ponieważ mam niską reputację. :( Może później.
metamaker,
2

Często jest użyteczne z punktu widzenia projektowania, aby móc oznaczyć rzeczy jako niezmienne. W ten sam sposób constzapewnia kompilator chroni i wskazuje, że stan nie powinien się zmienić, finalmoż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 finalzagwarantować, ż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 finalw 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 finalto 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 virtuali 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 virtualw 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::fjawnie używa się wirtualnego słowa kluczowego, Bar::fjest funkcją wirtualną. Słowo virtualkluczowe staje się wówczas opcjonalne. Dlatego może być sensowne Bar::fokreślenie jako final, nawet jeśli jest to funkcja wirtualna ( finalmoże być używana tylko w przypadku funkcji wirtualnych).

I niektórzy ludzie wolą, stylistycznie, jawnie wskazywać, że Bar::fjest wirtualny, tak jak:

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

Dla mnie to trochę zbędne stosowanie obu virtuali finalspecyfikatorów dla tej samej funkcji w tym kontekście (podobnie virtuali override), ale w tym przypadku jest to kwestia stylu. Niektóre osoby mogą uznać, że virtualprzekazuje tutaj coś cennego, podobnie jak w externprzypadku deklaracji funkcji z zewnętrznym powiązaniem (chociaż opcjonalnie nie ma innych kwalifikatorów powiązania).


źródło
Jak zamierzasz zastąpić privatemetodę w Vehicleklasie? Nie miałeś na myśli protectedzamiast tego?
Andy
3
@DavidPacker privatenie obejmuje nadpisywania (nieco sprzeczne z intuicją). Specyfikacje publiczne / chronione / prywatne dla funkcji wirtualnych mają zastosowanie tylko do dzwoniących, a nie do nadpisujących, mówiąc krótko. Klasa pochodna może zastępować funkcje wirtualne z klasy bazowej niezależnie od jej widoczności.
@DavidPacker protectedmoże mieć nieco bardziej intuicyjny sens. Po prostu wolę sięgać po możliwie najlepszą widoczność, kiedy tylko mogę. Myślę, że powodem tego, że język został zaprojektowany w ten sposób, jest to, że w przeciwnym razie prywatne funkcje wirtualnych członków nie miałyby żadnego sensu poza kontekstem przyjaźni, ponieważ żadna klasa oprócz znajomego nie byłaby w stanie ich zastąpić, gdyby specyfikatory dostępu miały znaczenie w kontekście nadpisywania, a nie tylko dzwonienia.
Myślałem, że ustawienie go na prywatny nawet się nie skompiluje. Na przykład ani Java, ani C # nie pozwalają na to. C ++ zaskakuje mnie każdego dnia. Nawet po 10 latach programowania.
Andy
@DavidPacker To mnie też potknęło, kiedy po raz pierwszy go spotkałem. Może powinienem to zrobić, protectedaby uniknąć mylenia innych. Skończyło się na tym, że dodałem komentarz, opisujący, w jaki sposób prywatne funkcje wirtualne mogą być nadal zastępowane.
0
  1. Umożliwia wiele optymalizacji, ponieważ w czasie kompilacji można wiedzieć, która funkcja jest wywoływana.

  2. Uważaj, rzucając słowo „zapach kodu”. „końcowy” nie uniemożliwia rozszerzenia klasy. Dwukrotnie kliknij „ostatnie” słowo, naciśnij backspace i przedłuż klasę. JEDNAK wersja jest doskonałą dokumentacją, że programista nie oczekuje, że zastąpisz funkcję, i dlatego też następny programista powinien być bardzo ostrożny, ponieważ klasa może przestać działać poprawnie, jeśli metoda końcowa zostanie w ten sposób nadpisana.

gnasher729
źródło
Dlaczego by finali virtualnigdy być używane w tym samym czasie?
Robert Harvey
1
Umożliwia wiele optymalizacji, ponieważ w czasie kompilacji można wiedzieć, która funkcja jest wywoływana. Czy możesz wyjaśnić, w jaki sposób lub podać odniesienie? JEDNAK wersja jest doskonałą dokumentacją, że programista nie oczekuje, że zastąpisz funkcję, i dlatego też następny programista powinien być bardzo ostrożny, ponieważ klasa może przestać działać poprawnie, jeśli metoda końcowa zostanie w ten sposób nadpisana. Czy to nie jest nieprzyjemny zapach?
metamaker
@RobertHarvey ABI stabilność. W każdym innym przypadku prawdopodobnie powinien to być finali override.
Deduplicator
@metamaker Miałem to samo pytanie, omówione w komentarzach tutaj - programmers.stackexchange.com/questions/256778/... - i zabawnie wpadłem na kilka innych dyskusji, tylko od czasu pytania! Zasadniczo chodzi o to, czy optymalizujący kompilator / linker może ustalić, czy referencja / wskaźnik może reprezentować klasę pochodną, ​​a zatem potrzebuje wirtualnego wywołania. Jeśli metody (lub klasy) nie da się w żaden sposób zastąpić poza statycznym typem odwołania, mogą one dewirtualizować: wywołać bezpośrednio. W razie wątpliwości - jak często - muszą zadzwonić wirtualnie. Deklaracja finalmoże naprawdę im pomóc
underscore_d