Parola chiave "virtuale" C ++ per funzioni in classi derivate. È necessario?


221

Con la definizione di struttura fornita di seguito ...

struct A {
    virtual void hello() = 0;
};

Approccio n. 1:

struct B : public A {
    virtual void hello() { ... }
};

Approccio n. 2:

struct B : public A {
    void hello() { ... }
};

C'è qualche differenza tra questi due modi per ignorare la funzione ciao?


65
In C ++ 11 puoi scrivere "void hello () override {}" per dichiarare esplicitamente che stai sovrascrivendo un metodo virtuale. Il compilatore avrà esito negativo se non esiste un metodo virtuale di base e ha la stessa leggibilità del posizionamento "virtuale" sulla classe discendente.
ShadowChaser

In realtà, nel C ++ 11 di gcc, scrivere void hello () override {} nella classe derivata va bene perché la classe base ha specificato che il metodo hello () è virtuale. In altre parole, l'uso della parola virtuale nella classe derivata non è necessario / obbligatorio, comunque per gcc / g ++. (Sto usando gcc versione 4.9.2 su un RPi 3) Ma è buona norma includere comunque la parola chiave virtuale nel metodo della classe derivata.
Sarà il

Risposte:


183

Sono esattamente gli stessi. Non vi è alcuna differenza tra loro se non che il primo approccio richiede una maggiore digitazione ed è potenzialmente più chiaro.


25
Questo è vero, ma la Guida alla portabilità di Mozilla C ++ consiglia di utilizzare sempre virtuale perché "alcuni compilatori" emettono avvisi in caso contrario. Peccato che non menzionino alcun esempio di tali compilatori.
Sergei Tachenov,

5
Vorrei anche aggiungere che contrassegnarlo esplicitamente come virtuale ti aiuterà a ricordare di rendere virtuale anche il distruttore.
lfalin,

1
Solo per citare, lo stesso vale per il distruttore virtuale
Atul,

6
@SergeyTachenov secondo il commento di clifford alla sua stessa risposta , un esempio di tali compilatori è armcc.
Ruslan,

4
@Rasmi, la nuova guida alla portabilità è qui , ma ora consiglia di utilizzare la overrideparola chiave.
Sergei Tachenov,

83

La "virtualità" di una funzione viene propagata implicitamente, tuttavia almeno un compilatore che utilizzo genererà un avviso se la virtualparola chiave non viene utilizzata in modo esplicito, quindi è possibile utilizzarla se non altro per mantenere silenzioso il compilatore.

Da un punto di vista puramente stilistico, inclusa la virtualparola chiave "pubblicizza" chiaramente il fatto all'utente che la funzione è virtuale. Questo sarà importante per chiunque ulteriore sottoclasse B senza dover controllare la definizione di A. Per le gerarchie di classi profonde, questo diventa particolarmente importante.


12
Quale compilatore è questo?
James McNellis,

35
@James: armcc (compilatore ARM per dispositivi ARM)
Clifford

55

La virtualparola chiave non è necessaria nella classe derivata. Ecco la documentazione di supporto, dal C ++ Draft Standard (N3337) (sottolineatura mia):

10.3 Funzioni virtuali

2 Se una funzione membro virtuale vfviene dichiarata in una classe Basee in una classe Derived, derivata direttamente o indirettamente da Base, una funzione membro vfcon lo stesso nome, elenco parametri-tipo (8.3.5), qualifica cv e qualificatore ref ( o assenza dello stesso) come Base::vfè dichiarato, quindi Derived::vfè anche virtuale ( indipendentemente dal fatto che sia così dichiarato ) e prevale Base::vf.


5
Questa è di gran lunga la migliore risposta qui.
Fantastico Mr Fox,

33

No, virtualnon è richiesta la parola chiave sulle sostituzioni di funzione virtuale delle classi derivate. Ma vale la pena menzionare una trappola correlata: un fallimento nel scavalcare una funzione virtuale.

L' errore di override si verifica se si intende sovrascrivere una funzione virtuale in una classe derivata, ma si commette un errore nella firma in modo che dichiari una nuova e diversa funzione virtuale. Questa funzione potrebbe essere un sovraccarico della funzione della classe base o potrebbe differire nel nome. Indipendentemente dal fatto che si usi o meno la virtualparola chiave nella dichiarazione di funzione di classe derivata, il compilatore non sarebbe in grado di dire che si intende sovrascrivere una funzione da una classe di base.

Questo errore è, tuttavia, risolto per fortuna dalla funzionalità del linguaggio di sostituzione esplicita C ++ 11 , che consente al codice sorgente di specificare chiaramente che una funzione membro è destinata a sovrascrivere una funzione di classe base:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

Il compilatore genererà un errore in fase di compilazione e l'errore di programmazione sarà immediatamente evidente (forse la funzione in Derivata avrebbe dovuto prendere floatcome argomento).

Fare riferimento a WP: C ++ 11 .


11

L'aggiunta della parola chiave "virtuale" è una buona pratica in quanto migliora la leggibilità, ma non è necessaria. Le funzioni dichiarate virtuali nella classe base e con la stessa firma nelle classi derivate sono considerate "virtuali" per impostazione predefinita.


7

Non c'è differenza per il compilatore, quando si scrive virtualnella classe derivata o si omette.

Ma devi guardare la classe base per ottenere queste informazioni. Pertanto, consiglierei di aggiungere la virtualparola chiave anche nella classe derivata, se si desidera mostrare all'umano che questa funzione è virtuale.


2

C'è una notevole differenza quando si hanno modelli e si inizia a prendere le classi di base come parametri del modello:

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

La parte divertente è che ora puoi definire le funzioni di interfaccia e non interfaccia in seguito alla definizione delle classi. Ciò è utile per le interfacce interworking tra le librerie (non fare affidamento su questo come processo di progettazione standard di una singola libreria). Non ti costa nulla permetterlo per tutte le tue classi: potresti anche typedefB a qualcosa, se lo desideri.

Nota che, se lo fai, potresti voler dichiarare anche copia / spostare costruttori come modelli: consentire di costruire da interfacce diverse ti permette di "lanciare" tra B<>tipi diversi .

È discutibile se è necessario aggiungere il supporto per const A&in t_hello(). La solita ragione di questa riscrittura è quella di passare dalla specializzazione basata sull'eredità a quella basata su modelli, principalmente per motivi di prestazioni. Se continui a supportare la vecchia interfaccia, difficilmente puoi rilevare (o scoraggiare) il vecchio utilizzo.


1

La virtualparola chiave deve essere aggiunta alle funzioni di una classe base per renderle sostituibili. Nel tuo esempio, struct Aè la classe base. virtualnon significa nulla per l'utilizzo di tali funzioni in una classe derivata. Tuttavia, se vuoi che anche la tua classe derivata sia una classe base e vuoi che quella funzione sia sovrascrivibile, allora dovresti metterla virtuallì.

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

Qui Ceredita da B, quindi Bnon è la classe di base (è anche una classe derivata), ed Cè la classe derivata. Il diagramma dell'ereditarietà è simile al seguente:

A
^
|
B
^
|
C

Quindi dovresti mettere le virtualfunzioni di fronte a potenziali classi base che possono avere figli. virtualconsente ai tuoi figli di ignorare le tue funzioni. Non c'è nulla di sbagliato nel mettere le virtualfunzioni davanti alle classi derivate, ma non è necessario. Si consiglia tuttavia, perché se qualcuno desidera ereditare dalla classe derivata, non sarebbe contento che il metodo di sostituzione non funzioni come previsto.

Quindi metti virtualdi fronte le funzioni di tutte le classi coinvolte nell'ereditarietà, a meno che tu non sappia per certo che la classe non avrà figli che avrebbero bisogno di scavalcare le funzioni della classe base. È buona pratica.


0

Includerò sicuramente la parola chiave virtuale per la classe figlio, perché

  • io. Leggibilità.
  • ii. Questa classe figlio può essere derivata più in basso, non vuoi che il costruttore dell'ulteriore classe derivata chiami questa funzione virtuale.

1
Penso che significhi che senza contrassegnare la funzione figlio come virtuale, un programmatore che in seguito derivi dalla classe figlio potrebbe non rendersi conto che la funzione in realtà è virtuale (perché non ha mai guardato la classe base) e potrebbe potenzialmente chiamarla durante la costruzione ( che può o meno fare la cosa giusta).
PfhorSlayer
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.