È spesso utile dal punto di vista del design essere in grado di contrassegnare le cose come immutabili. Allo stesso modo, const
fornisce le protezioni del compilatore e indica che uno stato non deve cambiare, final
può essere usato per indicare che il comportamento non dovrebbe cambiare ulteriormente nella gerarchia dell'ereditarietà.
Esempio
Prendi in considerazione un videogioco in cui i veicoli portano il giocatore da una posizione all'altra. Tutti i veicoli devono verificare per assicurarsi che siano in viaggio verso una posizione valida prima della partenza (assicurandosi che la base nella posizione non sia distrutta, ad es.). Possiamo iniziare utilizzando 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 tutti i veicoli volanti richiedono e hanno in comune è che devono sottoporsi a un controllo di sicurezza all'interno dell'hangar prima del decollo.
Qui possiamo utilizzare final
per garantire che tutti i veicoli volanti passeranno attraverso tale ispezione e anche comunicare 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 effettivamente estendendo la flessibilità del linguaggio dell'interfaccia non virtuale per fornire un comportamento uniforme nella gerarchia dell'ereditarietà (anche come ripensamento, contrastando il fragile problema della classe base) alle stesse funzioni virtuali. Inoltre, ci compriamo spazio per fare cambiamenti centrali che incidono su tutti i tipi di veicoli volanti come ripensamento senza modificare ogni implementazione del veicolo volante esistente.
Questo è uno di questi esempi di utilizzo final
. Ci sono contesti che incontrerai in cui semplicemente non ha senso che una funzione di membro virtuale venga ulteriormente ignorata; farlo potrebbe portare a un design fragile e a una violazione dei requisiti di progettazione.
Ecco dove final
è utile dal punto di vista progettuale / architettonico.
È anche utile dal punto di vista dell'ottimizzatore poiché fornisce all'ottimizzatore queste informazioni di progettazione che gli consentono di devirtualizzare le chiamate di funzione virtuale (eliminando l'overhead di invio dinamico e spesso più significativamente, eliminando una barriera di ottimizzazione tra chiamante e chiamata).
Domanda
Dai commenti:
Perché final e virtual sarebbero mai stati usati allo stesso tempo?
Non ha senso per una classe base alla radice di una gerarchia dichiarare una funzione come sia virtual
e final
. Questo mi sembra abbastanza sciocco, poiché costringerebbe sia il compilatore che il lettore umano a saltare attraverso cerchi inutili che possono essere evitati semplicemente evitando del virtual
tutto in tal 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 dall'utilizzo Bar::f
esplicito della parola chiave virtuale, Bar::f
è una funzione virtuale. La virtual
parola chiave diventa quindi facoltativa in questo caso. Quindi potrebbe avere senso Bar::f
essere specificato come final
, anche se è una funzione virtuale ( final
può essere utilizzata solo per 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 virtual
e gli final
identificatori 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 qualcosa di prezioso qui, proprio come usare extern
per le dichiarazioni di funzione con collegamento esterno (anche se è facoltativo privo di altri qualificatori di collegamento).