Qual è lo scopo della parola chiave "finale" in C ++ 11 per le funzioni?


143

Qual è lo scopo della finalparola chiave in C ++ 11 per le funzioni? Capisco che impedisce l'override delle funzioni da parte delle classi derivate, ma se è così, non è sufficiente dichiarare come non virtuali le tue finalfunzioni? C'è un'altra cosa che mi manca qui?


30
" non è sufficiente per dichiarare come non virtuali le tue funzioni " finali " " No, le funzioni prioritarie sono implicitamente virtuali sia che tu usi la virtualparola chiave o meno.
ildjarn,

13
@ildjarn non è vero se non sono stati dichiarati virtuali nella super classe, non puoi derivare da una classe e trasformare un metodo non virtuale in uno virtuale ..
Dan O

10
@DanO penso che non puoi ignorare ma puoi "nascondere" un metodo in quel modo .. il che porta a molti problemi in quanto le persone non intendono nascondere i metodi.
Alex Kremer,

16
@DanO: Se non è virtuale nella super classe, non sarebbe "prioritario".
ildjarn,

2
Ancora una volta, " scavalcamento " ha un significato specifico qui, che è quello di dare un comportamento polimorfico a una funzione virtuale. Nel tuo esempio funcnon è virtuale, quindi non c'è nulla da sovrascrivere e quindi nulla da contrassegnare come overrideo final.
ildjarn,

Risposte:


129

Ciò che ti manca, come già detto in un commento, è che se lo sei override di una funzione da una classe base, non è possibile contrassegnarla come non virtuale:

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

Grazie! questo è il punto che mi mancava: vale a dire che anche le tue classi "leaf" devono contrassegnare la loro funzione come virtuale anche se intendono sovrascrivere le funzioni e non essere
ignorate da

8
@lezebulon: le tue classi foglia non devono contrassegnare una funzione come virtuale se la super classe l'ha dichiarata virtuale.
Dan O

5
I metodi nelle classi foglia sono implicitamente virtuali se sono virtuali nella classe base. Penso che i compilatori dovrebbero avvisare se manca questo "virtuale" implicito.
Aaron McDaid l'

@AaronMcDaid: i compilatori di solito avvertono del codice che, essendo corretto, potrebbe causare confusione o errori. Non ho mai visto nessuno sorpreso da questa particolare caratteristica del linguaggio in un modo che potesse causare problemi, quindi non so davvero quanto utile possa essere quell'errore. Al contrario, dimenticare che virtualpuò causare errori e C ++ 11 ha aggiunto il overridetag a una funzione che rileverà quella situazione e non riuscirà a compilare quando si nasconde effettivamente una funzione che ha lo scopo di scavalcare
David Rodríguez - dribeas

1
Dalle note di modifica di GCC 4.9: "Nuovo modulo di analisi dell'ereditarietà dei tipi che migliora la devirtualizzazione. La devirtualizzazione ora tiene conto degli spazi dei nomi anonimi e della parola chiave finale C ++ 11" - quindi non è solo zucchero sintattico, ma ha anche un potenziale vantaggio di ottimizzazione.
kfsone,

127
  • Serve a impedire che una classe venga ereditata. Da Wikipedia :

    C ++ 11 aggiunge anche la possibilità di impedire l'ereditarietà dalle classi o semplicemente impedire i metodi di sostituzione nelle classi derivate. Questo viene fatto con l'identificatore speciale final. Per esempio:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • Viene anche utilizzato per contrassegnare una funzione virtuale in modo da impedire che venga sovrascritta nelle classi derivate:

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

Wikipedia sottolinea ulteriormente un punto interessante :

Si noti che overridené lo finalsono né le parole chiave della lingua. Sono tecnicamente identificatori; ottengono un significato speciale solo se utilizzati in contesti specifici . In qualsiasi altra posizione, possono essere identificatori validi.

Ciò significa che è consentito quanto segue:

int const final = 0;     // ok
int const override = 1;  // ok

1
grazie, ma ho dimenticato di dire che la mia domanda riguardava l'uso del metodo "finale"
lezebulon

L'hai menzionato @lezebulon :-) "qual è lo scopo della parola chiave" finale "in C ++ 11 per le funzioni ". (la mia enfasi)
Aaron McDaid l'

L'hai modificato? Non vedo alcun messaggio che dice "modificato x minuti fa da lezebulon". Come è successo? Forse l'hai modificato molto rapidamente dopo averlo inviato?
Aaron McDaid l'

5
@Aaron: le modifiche apportate entro cinque minuti dalla pubblicazione non si riflettono nella cronologia delle revisioni.
ildjarn,

@Nawaz: perché non sono parole chiave solo identificatori? È per motivi di compatibilità significa che è possibile che il codice preesistente prima di C ++ 11 usi final & override per altri scopi?
Distruttore,

45

"final" consente inoltre a un'ottimizzazione del compilatore di ignorare la chiamata indiretta:

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

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

  void Blah( void ) { DoSomething(); }

};

con "final", il compilatore può chiamare CDerived::DoSomething()direttamente dall'interno Blah()o persino in linea. Senza di essa, deve generare una chiamata indiretta all'interno di Blah()perché Blah()potrebbe essere chiamata all'interno di una classe derivata che ha scavalcato DoSomething().


29

Niente da aggiungere agli aspetti semantici di "final".

Ma vorrei aggiungere al commento di chris green che "final" potrebbe diventare una tecnica di ottimizzazione del compilatore molto importante in un futuro non molto lontano. Non solo nel semplice caso che ha citato, ma anche per gerarchie di classi del mondo reale più complesse che possono essere "chiuse" da "final", consentendo così ai compilatori di generare codice di dispacciamento più efficiente rispetto al solito approccio vtable.

Uno svantaggio chiave di vtables è che per qualsiasi oggetto virtuale (supponendo 64 bit su una tipica CPU Intel) il solo puntatore consuma fino al 25% (8 di 64 byte) di una linea cache. Nel tipo di applicazioni che mi piace scrivere, questo fa molto male. (E dalla mia esperienza è l'argomento n. 1 contro C ++ da un punto di vista purista delle prestazioni, vale a dire dai programmatori C.)

Nelle applicazioni che richiedono prestazioni estreme, il che non è così insolito per il C ++, questo potrebbe davvero diventare fantastico, senza la necessità di risolvere questo problema manualmente in stile C o giocoleria strana con Template.

Questa tecnica è conosciuta come Devirtualization . Un termine che vale la pena ricordare. :-)

C'è un grande recente discorso di Andrei Alexandrescu che spiega abbastanza bene come risolvere queste situazioni oggi e come "finale" potrebbe far parte della risoluzione di casi simili "automaticamente" in futuro (discussi con gli ascoltatori):

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


23
8 è il 25% di 64?
ildjarn,

6
Qualcuno sa di un compilatore che li utilizza ora?
Vincent Fourmond,

stessa cosa che voglio dire.
crazii,

8

Final non può essere applicato a funzioni non virtuali.

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

Non sarebbe molto significativo essere in grado di contrassegnare un metodo non virtuale come 'finale'. Dato

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()chiamerà sempre A::foo.

Ma, se fosse A :: foo virtual, allora B :: foo lo avrebbe annullato. Questo potrebbe essere indesiderabile, e quindi avrebbe senso rendere definitiva la funzione virtuale.

La domanda è: perché consentire le funzioni finali su virtuali. Se hai una gerarchia profonda:

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

Quindi finalmette un "piano" su quanto può essere fatto l'override. Altre classi possono estendere A e B e sovrascriverle foo, ma una classe estende C quindi non è consentita.

Quindi probabilmente non ha senso rendere il finalpiumino di "livello superiore" , ma potrebbe avere un senso più in basso.

(Penso che ci sia spazio per estendere le parole final e sovrascrivere ai membri non virtuali. Avrebbero comunque un significato diverso.)


grazie per l'esempio, è qualcosa di cui non ero sicuro. Ma ancora: qual è il punto di avere una funzione finale (e virtuale)? Fondamentalmente non saresti mai in grado di usare il fatto che la funzione è virtuale poiché non può essere
annullata

@lezebulon, ho modificato la mia domanda. Ma poi ho notato la risposta di DanO: è una buona risposta chiara a quello che stavo cercando di dire.
Aaron McDaid l'

Non sono un esperto, ma sento che a volte può avere senso svolgere una funzione di alto livello final. Ad esempio, se sai di voler fare tutto ciò Shape, foo()qualcosa di predefinito e definito che nessuna forma derivata dovrebbe modificare. Oppure, mi sbaglio e c'è un modello migliore da impiegare per quel caso? EDIT: Oh, forse perché in quel caso, non si dovrebbe semplicemente foo() virtualiniziare al livello più alto ? Tuttavia, può essere nascosto, anche se chiamato correttamente (polimorficamente) tramite Shape*...
Andrew Cheong

8

Un caso d'uso per la parola chiave "finale" a cui sono affezionato è il seguente:

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

1
Sì, questo è essenzialmente un esempio del modello Metodo modello. E prima del C ++ 11, è sempre stato il TMP a farmi desiderare che il C ++ avesse una funzionalità di linguaggio come "final", come ha fatto Java.
Kaitain,

6

final aggiunge un intento esplicito a non sovrascrivere la tua funzione e causerà un errore del compilatore in caso di violazione:

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

Allo stato attuale del codice, viene compilato e B::fooignorato A::foo. B::fooè anche virtuale, a proposito. Tuttavia, se cambiamo il numero 1 in virtual int foo() final, questo è un errore del compilatore e non ci è permesso di sovrascrivereA::foo ulteriormente nelle classi derivate.

Si noti che ciò non ci consente di "riaprire" una nuova gerarchia, vale a dire che non c'è modo di creare B::foouna nuova funzione non correlata che possa essere indipendente alla testa di una nuova gerarchia virtuale. Una volta che una funzione è definitiva, non può più essere dichiarata in nessuna classe derivata.


5

La parola chiave finale consente di dichiarare un metodo virtuale, sovrascriverlo N volte e quindi imporre che "questo non può più essere sovrascritto". Sarebbe utile nel limitare l'uso della tua classe derivata, in modo che tu possa dire "So che la mia superclasse ti consente di ignorare questo, ma se vuoi derivare da me, non puoi!".

struct Foo
{
   virtual void DoStuff();
}

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

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

Come hanno sottolineato altri poster, non può essere applicato a funzioni non virtuali.

Uno scopo della parola chiave finale è impedire l'override accidentale di un metodo. Nel mio esempio, DoStuff () potrebbe essere stata una funzione di supporto che la classe derivata deve semplicemente rinominare per ottenere un comportamento corretto. Senza final, l'errore non sarebbe stato scoperto fino al test.


1

La parola chiave finale in C ++ quando aggiunta a una funzione, impedisce che venga sovrascritta da una classe di base. Inoltre, quando aggiunto a una classe impedisce l'ereditarietà di qualsiasi tipo. Considera l'esempio seguente che mostra l'uso dell'identificatore finale. Questo programma fallisce nella compilazione.

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

Anche:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}

0

Supplemento alla risposta di Mario Knezović:

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

Il codice sopra mostra la teoria, ma in realtà non testato su compilatori reali. Molto apprezzato se qualcuno incolla un output disassemblato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.