In C ++, quando dovrei usare final nella dichiarazione del metodo virtuale?


11

So che la finalparola chiave viene utilizzata per impedire che il metodo virtuale venga ignorato dalle classi derivate. Tuttavia, non riesco a trovare alcun esempio utile quando dovrei davvero usare la finalparola chiave con virtualmetodo. Ancora di più, sembra che l'utilizzo di finalmetodi virtuali sia un cattivo odore poiché non consente ai programmatori di estendere la classe in futuro.

La mia domanda è la prossima:

C'è qualche caso utile in cui dovrei davvero usare finalnella virtualdichiarazione del metodo?


Per tutti gli stessi motivi per cui i metodi Java sono stati resi definitivi?
Ordous,

In Java tutti i metodi sono virtuali. Il metodo finale in classe base può migliorare le prestazioni. Il C ++ ha metodi non virtuali, quindi non è un caso per questo linguaggio. Conosci altri buoni casi? Mi piacerebbe ascoltarli. :)
metamaker

Suppongo di non essere molto bravo a spiegare le cose - stavo cercando di dire esattamente la stessa cosa di Mike Nakis.
Ordous,

Risposte:


12

Un riepilogo di ciò che fa la finalparola chiave: diciamo che abbiamo classe base Ae classe derivata B. La funzione f()può essere dichiarata in Aas virtual, il che significa che la classe Bpuò sovrascriverla. Ma allora la classe Bpotrebbe desiderare che qualsiasi classe da cui deriva ulteriormente Bnon sia in grado di scavalcare f(). Questo è quando dobbiamo dichiarare f()come finalin B. Senza la finalparola chiave, una volta che si definisce un metodo come virtuale, ogni classe derivata sarebbe stato libero di ignorarlo. La finalparola chiave viene utilizzata per porre fine a questa libertà.

Un esempio del perché avresti bisogno di una cosa del genere: Supponiamo che la classe Adefinisca una funzione virtuale prepareEnvelope(), e la classe la Bsovrascriva e la implementi come una sequenza di chiamate ai propri metodi virtuali stuffEnvelope(), lickEnvelope()e sealEnvelope(). La classe Bintende consentire alle classi derivate di sovrascrivere questi metodi virtuali per fornire le proprie implementazioni, ma la classe Bnon vuole consentire a nessuna classe derivata di sovrascrivere prepareEnvelope()e quindi modificare l'ordine delle cose, leccare, sigillare o omettere di invocarne una. Quindi, in questo caso la classe Bdichiara prepareEnvelope()come finale.


Innanzitutto, grazie per la risposta, ma non è esattamente quello che ho scritto in una prima frase della mia domanda? Quello di cui ho davvero bisogno è un caso in cui class B might wish that any class which is further derived from B should not be able to override f()? Esiste un caso reale in cui tale classe B e il metodo f () esistono e si adattano bene?
Metamaker,

Sì, mi dispiace, aspetta, sto scrivendo un esempio adesso.
Mike Nakis,

@metamaker eccoti.
Mike Nakis,

1
Davvero un buon esempio, questa è la risposta migliore finora. Grazie!
Metamaker,

1
Quindi grazie per aver perso tempo. Non sono riuscito a trovare un buon esempio su Internet, ho perso molto tempo a cercare. Vorrei inserire +1 per la risposta, ma non posso farlo poiché ho una bassa reputazione. :( Forse più tardi.
metamaker

2

È spesso utile dal punto di vista del design essere in grado di contrassegnare le cose come immutabili. Allo stesso modo, constfornisce le protezioni del compilatore e indica che uno stato non deve cambiare, finalpuò 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 finalper 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 finalin 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 virtuale 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 virtualtutto 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::fesplicito della parola chiave virtuale, Bar::fè una funzione virtuale. La virtualparola chiave diventa quindi facoltativa in questo caso. Quindi potrebbe avere senso Bar::fessere specificato come final, anche se è una funzione virtuale ( finalpuò 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 virtuale gli finalidentificatori per la stessa funzione in questo contesto (allo stesso modo virtuale override), ma in questo caso è una questione di stile. Alcune persone potrebbero scoprire che virtualcomunica qualcosa di prezioso qui, proprio come usare externper le dichiarazioni di funzione con collegamento esterno (anche se è facoltativo privo di altri qualificatori di collegamento).


Come pensi di sovrascrivere il privatemetodo all'interno della tua Vehicleclasse? Non intendevi protectedinvece?
Andy,

3
@DavidPacker privatenon si estende al superamento (un po 'controintuitivo). Gli identificatori pubblici / protetti / privati ​​per le funzioni virtuali si applicano solo ai chiamanti e non agli overrider, in parole povere. Una classe derivata può sovrascrivere le funzioni virtuali dalla sua classe base indipendentemente dalla sua visibilità.

@DavidPacker protectedpotrebbe avere un senso un po 'più intuitivo. Preferisco solo raggiungere la visibilità più bassa possibile ogni volta che posso. Penso che il motivo per cui il linguaggio sia progettato in questo modo sia che, altrimenti, le funzioni dei membri virtuali privati ​​non avrebbero alcun senso al di fuori del contesto dell'amicizia, dal momento che nessuna classe se non un amico sarebbe quindi in grado di sovrascriverle se gli identificatori di accesso fossero importanti nel contesto di ignorare e non solo chiamare.

Pensavo che impostarlo su privato non si sarebbe nemmeno compilato. Ad esempio, né Java né C # lo consentono. Il C ++ mi sorprende ogni giorno. Anche dopo 10 anni di programmazione.
Andy,

@DavidPacker Mi ha inciampato anche quando l'ho incontrato per la prima volta. Forse dovrei solo farlo protectedper evitare di confondere gli altri. Ho finito per mettere un commento, almeno, descrivendo come le funzioni virtuali private possono ancora essere ignorate.

0
  1. Permette molte ottimizzazioni, perché può essere noto in fase di compilazione quale funzione viene chiamata.

  2. Fai attenzione a gettare la parola "odore di codice". "final" non rende impossibile l'estensione della classe. Fare doppio clic sulla parola "finale", premere backspace ed estendere la classe. TUTTAVIA final è un'eccellente documentazione che lo sviluppatore non si aspetta che tu abbia la precedenza sulla funzione e che per questo lo sviluppatore successivo dovrebbe fare molta attenzione, perché la classe potrebbe smettere di funzionare correttamente se il metodo finale viene ignorato in quel modo.


Perché finale virtualmai dovrebbero essere usati allo stesso tempo?
Robert Harvey,

1
Permette molte ottimizzazioni, perché può essere noto in fase di compilazione quale funzione viene chiamata. Puoi spiegare come o fornire un riferimento? TUTTAVIA final è un'eccellente documentazione che lo sviluppatore non si aspetta che tu abbia la precedenza sulla funzione e che a causa di ciò lo sviluppatore successivo dovrebbe fare molta attenzione, perché la classe potrebbe smettere di funzionare correttamente se il metodo finale viene ignorato in quel modo. Non è un cattivo odore?
Metamaker,

Stabilità ABI @RobertHarvey. In ogni altro caso, probabilmente dovrebbe essere finale override.
Deduplicatore

@metamaker Ho avuto lo stesso Q, discusso nei commenti qui - programmers.stackexchange.com/questions/256778/… - e stranamente sono inciampato in molte altre discussioni, solo da quando ho chiesto! Fondamentalmente, si tratta se un compilatore / linker ottimizzante può determinare se un riferimento / puntatore potrebbe rappresentare una classe derivata e quindi necessitare di una chiamata virtuale. Se il metodo (o la classe) non può essere sovrascritto al di là del proprio tipo statico di riferimento, possono devirtualizzare: chiamare direttamente. Se c'è qualche dubbio - come spesso - devono chiamare virtualmente. La dichiarazione finalpuò davvero aiutarli
underscore_d
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.