Jaki jest cel słowa kluczowego „final” w C ++ 11 dla funkcji?

143

Jaki jest cel finalsłowa kluczowego w C ++ 11 dla funkcji? Rozumiem, że zapobiega to zastępowaniu funkcji przez klasy pochodne, ale jeśli tak jest, czy nie wystarczy zadeklarować funkcje niewirtualne jako niewirtualne final? Czy jest coś, czego tu brakuje?

lezebulon
źródło
30
„Czy nie wystarczy zadeklarować jako niewirtualne swoje„ końcowe ”funkcje? Nie, funkcje nadpisujące są domyślnie wirtualne, niezależnie od tego, czy używasz virtualsłowa kluczowego, czy nie.
ildjarn
13
@ildjarn to nieprawda, jeśli nie zostały zadeklarowane jako wirtualne w klasie Super, nie można wyprowadzić z klasy i przekształcić metody non-wirtualny w wirtualnym ..
Dan O
10
@DanO Myślę, że nie da się tego zmienić, ale w ten sposób można „ukryć” metodę… co prowadzi do wielu problemów, ponieważ ludzie nie chcą ukrywać metod.
Alex Kremer
16
@DanO: Jeśli nie jest wirtualny w superklasie, to nie byłby „nadpisujący”.
ildjarn
2
Ponownie, „ nadpisywanie ” ma tutaj specyficzne znaczenie, które polega na nadaniu funkcji wirtualnej zachowania polimorficznego. W twoim przykładzie funcnie jest wirtualny, więc nie ma nic do zastąpienia, a zatem nie ma nic do oznaczenia jako overridelub final.
ildjarn

Odpowiedzi:

129

To, czego ci brakuje, jak idljarn już wspomniano w komentarzu, to fakt, że jeśli nadpisujesz funkcję z klasy bazowej, nie możesz oznaczyć jej jako niewirtualnej:

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
źródło
Dzięki! to jest ten punkt, którego mi brakowało: tzn. nawet twoje "liście" klasy muszą oznaczyć swoją funkcję jako wirtualną, nawet jeśli zamierzają nadpisać funkcje i nie mogą być nadpisywane same
lezebulon
8
@lezebulon: Twoje klasy liści nie muszą oznaczać funkcji jako wirtualnej, jeśli superklasa zadeklarowała ją jako wirtualną.
Dan O
5
Metody w klasach liści są niejawnie wirtualne, jeśli są wirtualne w klasie bazowej. Myślę, że kompilatory powinny ostrzec, jeśli brakuje tego ukrytego „wirtualnego”.
Aaron McDaid
@AaronMcDaid: Kompilatory zwykle ostrzegają przed kodem, który będąc poprawnym, może powodować zamieszanie lub błędy. Nigdy nie widziałem nikogo zaskoczonego tą szczególną cechą języka w sposób, który mógłby spowodować jakikolwiek problem, więc tak naprawdę nie wiem, jak użyteczny może być ten błąd. Wręcz przeciwnie, zapomnienie o virtualmoże powodować błędy, a C ++ 11 dodał overrideznacznik do funkcji, która wykryje tę sytuację i nie uda się skompilować, gdy funkcja, która ma zastąpić, faktycznie się ukrywa
David Rodríguez - dribeas
1
Z notatek dotyczących zmian w GCC 4.9: „Nowy moduł analizy dziedziczenia typu poprawiający dewirtualizację. Devirtualizacja uwzględnia teraz anonimowe przestrzenie nazw i końcowe słowo kluczowe w C ++ 11” - więc nie jest to tylko cukier syntaktyczny, ale ma również potencjalną korzyść w zakresie optymalizacji.
kfsone
126
  • Ma to na celu zapobieganie dziedziczeniu klasy. Z Wikipedii :

    C ++ 11 dodaje również możliwość zapobiegania dziedziczeniu po klasach lub po prostu zapobieganiu zastępowaniu metod w klasach pochodnych. Odbywa się to za pomocą specjalnego identyfikatora final. Na przykład:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • Służy również do oznaczania funkcji wirtualnej, aby zapobiec jej przesłonięciu w klasach pochodnych:

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

Wikipedia przedstawia ponadto interesujący punkt :

Zwróć uwagę, że overrideani finalsłowa kluczowe nie są językowe. Są to identyfikatory techniczne; zyskują specjalne znaczenie tylko wtedy, gdy są używane w tych określonych kontekstach . W każdym innym miejscu mogą to być prawidłowe identyfikatory.

Oznacza to, że dozwolone są następujące czynności:

int const final = 0;     // ok
int const override = 1;  // ok
Nawaz
źródło
1
dzięki, ale zapomniałem wspomnieć, że moje pytanie dotyczyło użycia „finału” z metodami
lezebulon
Wspomniałeś o tym @lezebulon :-) "jaki jest cel słowa kluczowego" final "w C ++ 11 dla funkcji ". (moje wyróżnienie)
Aaron McDaid
Edytowałeś to? Nie widzę żadnego komunikatu o treści „edytowane x minut temu przez lezebulon”. Jak to się stało? Może po przesłaniu bardzo szybko go zredagowałeś?
Aaron McDaid
5
@Aaron: Zmiany wprowadzone w ciągu pięciu minut po opublikowaniu nie są odzwierciedlane w historii zmian.
ildjarn
@Nawaz: dlaczego nie są słowami kluczowymi tylko specyfikatorami? Czy ze względu na kompatybilność jest możliwe, że istniejący kod przed C ++ 11 używa final & override do innych celów?
Destructor
45

„final” pozwala również optymalizacji kompilatora na ominięcie wywołania pośredniego:

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

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

  void Blah( void ) { DoSomething(); }

};

z "final" kompilator może wywołać CDerived::DoSomething()bezpośrednio z wewnątrz Blah()lub nawet w tekście. Bez tego musi wygenerować pośrednie wywołanie wewnątrz, Blah()ponieważ Blah()mogłoby zostać wywołane wewnątrz klasy pochodnej, która została przesłonięta DoSomething().

Chris Green
źródło
29

Nie ma nic do dodania do semantycznych aspektów „końcowego”.

Chciałbym jednak dodać do komentarza Chris Greena, że ​​„wersja ostateczna” może stać się bardzo ważną techniką optymalizacji kompilatora w nie tak odległej przyszłości. Nie tylko w prostym przypadku, o którym wspomniał, ale także w przypadku bardziej złożonych hierarchii klas w świecie rzeczywistym, które można „zamknąć” za pomocą „wersji ostatecznej”, umożliwiając kompilatorom generowanie wydajniejszego kodu rozsyłającego niż w przypadku zwykłego podejścia vtable.

Jedną z kluczowych wad vtables jest to, że dla każdego takiego wirtualnego obiektu (przy założeniu 64-bitowego na typowym procesorze Intela) sam wskaźnik zjada 25% (8 z 64 bajtów) linii pamięci podręcznej. W aplikacjach, które lubię pisać, bardzo boli. (I z mojego doświadczenia wynika, że ​​jest to argument nr 1 przeciwko C ++ z purystycznego punktu widzenia wydajności, tj. Przez programistów C).

W aplikacjach, które wymagają ekstremalnej wydajności, co nie jest tak niezwykłe w C ++, może to rzeczywiście stać się niesamowite, nie wymagając ręcznego obejścia tego problemu w stylu C lub dziwnego żonglowania szablonami.

Ta technika jest znana jako dewirtualizacja . Termin, który warto zapamiętać. :-)

Niedawne przemówienie Andrei Alexandrescu wyjaśnia, w jaki sposób można dziś obejść takie sytuacje i jak „ostateczne” może być częścią rozwiązywania podobnych przypadków „automatycznie” w przyszłości (omówione ze słuchaczami):

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

Mario Knezović
źródło
23
8 to 25% z 64?
ildjarn
6
Czy ktoś zna kompilator, który teraz z nich korzysta?
Vincent Fourmond
to samo, co chcę powiedzieć.
crazii
8

Finału nie można zastosować do funkcji niewirtualnych.

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

Możliwość oznaczenia metody niewirtualnej jako „ostatecznej” nie miałaby większego sensu. Dany

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()zawsze zadzwonię A::foo.

Ale jeśli A virtual:: foo byłby, to B :: foo by go zastąpił. Może to być niepożądane, dlatego też miałoby sens, aby funkcja wirtualna była ostateczna.

Pytanie brzmi jednak, dlaczego pozwalają na końcowy funkcji wirtualnych. Jeśli masz głęboką hierarchię:

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 */ };

Następnie finalustawia „podłogę” na temat tego, ile można zignorować. Inne klasy mogą rozszerzać A i B i zastępować je foo, ale jeśli klasa rozszerza C, to nie jest to dozwolone.

Więc prawdopodobnie nie ma sensu tworzyć foo „najwyższego poziomu” final, ale może mieć sens niżej.

(Myślę jednak, że jest miejsce, aby rozszerzyć słowa końcowe i zastąpić je członkami niewirtualnymi. Miałyby jednak inne znaczenie).

Aaron McDaid
źródło
dzięki za przykład, tego nie byłem pewien. Ale nadal: po co mieć ostatnią (i wirtualną) funkcję? Zasadniczo nigdy nie
byłbyś w
@lezebulon, zredagowałem moje pytanie. Ale potem zauważyłem odpowiedź Dana - to dobra, jasna odpowiedź na to, co próbowałem powiedzieć.
Aaron McDaid
Nie jestem ekspertem, ale wydaje mi się, że czasem warto pełnić funkcję na najwyższym poziomie final. Na przykład, jeśli wiesz, że chcesz, aby wszyscy Shape- foo()coś wstępnie zdefiniowanego i określonego, którego żaden kształt pochodny nie powinien modyfikować. A może się mylę i jest lepszy wzorzec do zastosowania w tym przypadku? EDYCJA: Och, może dlatego, że w takim przypadku po prostu nie powinno się po prostu foo() virtualzaczynać od najwyższego poziomu ? Ale mimo to, może być ukryte, nawet jeśli nazywa się poprawnie (polimorficznie) poprzez Shape*...
Andrew Cheong
8

Przykład użycia „końcowego” słowa kluczowego, które lubię, jest następujący:

// 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
źródło
1
Tak, jest to zasadniczo przykład wzorca metody szablonowej. A przed C ++ 11 zawsze to TMP sprawiało, że chciałem, aby C ++ posiadał funkcję języka, taką jak „wersja ostateczna”, tak jak Java.
Kaitain
6

final dodaje wyraźny zamiar nie zastępowania funkcji i spowoduje błąd kompilatora, jeśli zostanie to naruszone:

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

W obecnym stanie kod kompiluje się i B::foozastępuje A::foo. B::foojest również wirtualny. Jeśli jednak zmienimy # 1 na virtual int foo() final, jest to błąd kompilatora i nie możemy go przesłonićA::foo dalej w klasach pochodnych.

Zauważ, że nie pozwala nam to na „ponowne otwarcie” nowej hierarchii, tj. Nie ma sposobu na utworzenie B::foonowej, niepowiązanej funkcji, która mogłaby być niezależnie na czele nowej wirtualnej hierarchii. Gdy funkcja jest ostateczna, nie można jej już nigdy zadeklarować w żadnej klasie pochodnej.

Kerrek SB
źródło
5

Ostatnie słowo kluczowe pozwala zadeklarować metodę wirtualną, nadpisać ją N razy, a następnie nakazać, aby „tego nie można już przesłonić”. Przydałoby się to w ograniczaniu użycia Twojej klasy pochodnej, abyś mógł powiedzieć „Wiem, że moja superklasa pozwala ci to zastąpić, ale jeśli chcesz wyprowadzić ode mnie, nie możesz!”.

struct Foo
{
   virtual void DoStuff();
}

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

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

Jak wskazywały inne plakaty, nie można go zastosować do funkcji niewirtualnych.

Jednym z celów końcowego słowa kluczowego jest zapobieganie przypadkowemu nadpisaniu metody. W moim przykładzie DoStuff () mogła być funkcją pomocniczą, której klasa pochodna musi po prostu zmienić nazwę, aby uzyskać prawidłowe zachowanie. Bez wersji ostatecznej błąd nie zostałby wykryty przed testami.

Dan O
źródło
1

Ostatnie słowo kluczowe w C ++ dodane do funkcji zapobiega jej przesłonięciu przez klasę bazową. Również po dodaniu do klasy zapobiega dziedziczeniu jakiegokolwiek typu. Rozważmy następujący przykład, który pokazuje użycie końcowego specyfikatora. Ten program nie kompiluje się.

#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;
}

Również:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}
Krishna Ganeriwal
źródło
0

Uzupełnienie odpowiedzi Mario Knezovicia:

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();

Powyższy kod przedstawia teorię, ale nie został faktycznie przetestowany na prawdziwych kompilatorach. Bardzo cenione, jeśli ktoś wklei zdemontowane wyjście.

crazii
źródło