Spesso è utile dal punto di vista del design essere in grado di contrassegnare le cose come immutabili. Allo stesso modo const
fornisce guard del compilatore e indica che uno stato non dovrebbe cambiare, final
può essere usato per indicare che il comportamento non dovrebbe cambiare ulteriormente nella gerarchia dell'ereditarietà.
Esempio
Considera un videogioco in cui i veicoli portano il giocatore da una posizione all'altra. Tutti i veicoli devono verificare che stiano viaggiando verso una posizione valida prima della partenza (assicurandosi che la base nel luogo non venga distrutta, ad es.). Possiamo iniziare a utilizzare l'idioma di interfaccia non virtuale (NVI) per garantire che questo controllo venga effettuato indipendentemente dal veicolo.
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;
};
Ora diciamo che abbiamo veicoli volanti nel nostro gioco, e qualcosa che i tutti veicoli volanti richiedono e hanno in comune è che devono passare attraverso un controllo di sicurezza all'interno dell'hangar prima del decollo .
Qui possiamo utilizzare final
per garantire che tutti i veicoli volanti effettuino tale ispezione e comunichino anche questo requisito di progettazione dei veicoli volanti.
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;
};
Usando final
in questo modo, stiamo efficacemente estendendo la flessibilità dell'interfaccia idioma non virtuale per fornire un comportamento uniforme verso il basso della gerarchia dell'ereditarietà (anche come ripensamento, contrastando il fragile problema della classe base) a virtuale funziona da solo Inoltre, ci compriamo una sala di manovra per apportare modifiche centrali che influiscono su tutti i tipi di veicoli volanti come un ripensamento senza modificare ogni singola implementazione di veicolo volante esistente.
Questo è uno di questi esempi di utilizzo di final
. Ci sono contesti che incontrerai in cui semplicemente non ha senso che una funzione membro virtuale venga sovrascritta più avanti - farlo potrebbe portare a un design fragile e una violazione dei tuoi requisiti di progettazione.
Ecco dove final
è utile dal punto di vista del progetto / architettonico.
È anche utile dal punto di vista di un ottimizzatore poiché fornisce all'ottimizzatore queste informazioni di progettazione che gli consentono di virtualizzare le chiamate di funzioni virtuali (eliminando il sovraccarico di dispacciamento dinamico e spesso in modo più significativo, eliminando una barriera di ottimizzazione tra il chiamante e il chiamante). p>
Domanda
Dai commenti:
Why would final and virtual ever be used at the same time?
Non ha senso che una classe base nella radice di una gerarchia dichiari una funzione come virtual
e final
. Mi sembra abbastanza sciocco, perché farebbe in modo che sia il compilatore che il lettore umano debbano saltare attraverso cerchi inutili che possono essere evitati semplicemente evitando virtual
a titolo definitivo in questo caso. Tuttavia, le sottoclassi ereditano le funzioni dei membri virtuali in questo modo:
struct Foo
{
virtual ~Foo() {}
virtual void f() = 0;
};
struct Bar: Foo
{
/*implicitly virtual*/ void f() final {...}
};
In questo caso, indipendentemente dal fatto che Bar::f
utilizzi esplicitamente la parola chiave virtuale, Bar::f
è una funzione virtuale. La parola chiave virtual
diventa quindi opzionale in questo caso. Quindi potrebbe avere senso che Bar::f
sia specificato come final
, anche se è una funzione virtuale ( final
può solo essere usato per le funzioni virtuali).
E alcune persone potrebbero preferire, stilisticamente, di indicare esplicitamente che Bar::f
è virtuale, in questo modo:
struct Bar: Foo
{
virtual void f() final {...}
};
Per me è un po 'ridondante usare entrambi gli specificatori virtual
e final
per la stessa funzione in questo contesto (allo stesso modo virtual
e override
), ma in questo caso è una questione di stile. Alcune persone potrebbero scoprire che virtual
comunica qui qualcosa di prezioso, proprio come usare extern
per le dichiarazioni di funzioni con linkage esterno (anche se è facoltativo privo di altri qualificatori di linkage).