2009-04-09 12 views
145

Ho questa domanda quando ho ricevuto un commento di revisione del codice che dice che le funzioni virtuali non devono essere in linea.Le funzioni virtuali in linea sono davvero un non senso?

Ho pensato che le funzioni virtuali incorporate potrebbero rivelarsi utili in scenari in cui le funzioni vengono richiamate direttamente sugli oggetti. Ma la contro-argomentazione mi è venuta in mente: perché si vorrebbe definire virtuale e quindi usare gli oggetti per chiamare i metodi?

È meglio non utilizzare funzioni virtuali in linea, poiché non vengono quasi mai espanse?

Snippet di codice che ho usato per l'analisi:

class Temp 
{ 
public: 

    virtual ~Temp() 
    { 
    } 
    virtual void myVirtualFunction() const 
    { 
     cout<<"Temp::myVirtualFunction"<<endl; 
    } 

}; 

class TempDerived : public Temp 
{ 
public: 

    void myVirtualFunction() const 
    { 
     cout<<"TempDerived::myVirtualFunction"<<endl; 
    } 

}; 

int main(void) 
{ 
    TempDerived aDerivedObj; 
    //Compiler thinks it's safe to expand the virtual functions 
    aDerivedObj.myVirtualFunction(); 

    //type of object Temp points to is always known; 
    //does compiler still expand virtual functions? 
    //I doubt compiler would be this much intelligent! 
    Temp* pTemp = &aDerivedObj; 
    pTemp->myVirtualFunction(); 

    return 0; 
} 
+0

consideri la compilazione di un esempio con qualunque switch sia necessario per ottenere un elenco assemblatore e quindi mostrare il revisore del codice che, in effetti, il compilatore può integrare funzioni virtuali. –

+0

Di solito, questo non sarà in linea, perché si sta chiamando la funzione virtuale in base alla classe base. Sebbene dipenda solo da quanto intelligente sia il compilatore. Se fosse in grado di indicare che 'pTemp-> myVirtualFunction()' potrebbe essere risolto come chiamata non virtuale, potrebbe avere quella chiamata in linea. Questa chiamata referenziata è integrata da g ++ 3.4.2: 'TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction(); 'Il tuo codice non lo è. – doc

+0

Una cosa che gcc fa effettivamente è confrontare la voce vtable con un simbolo specifico e quindi utilizzare una variante inline in un ciclo se corrisponde. Ciò è particolarmente utile se la funzione inline è vuota e il loop può essere eliminato in questo caso. –

risposta

125

Le funzioni virtuali possono essere talvolta allineate. Un estratto dalla eccellente C++ faq:

"L'unica volta una linea chiamata virtuale può essere inline è quando il compilatore conosce la 'classe esatta' dell'oggetto che è la destinazione della chiamata funzione virtuale . Ciò può avvenire solo quando il compilatore ha un oggetto effettivo anziché un puntatore o riferimento a un oggetto. Vale a dire, sia con un oggetto locale, un oggetto globale/statico, o un oggetto completamente contenuta all'interno di un composito ".

+6

Vero, ma vale la pena ricordare che il compilatore è libero di ignorare lo specificatore in linea anche se la chiamata può essere risolta in fase di compilazione e può essere sottolineata. – sharptooth

+6

Un'altra situazione in cui penso che l'inlining possa accadere è quando chiameresti il ​​metodo ad esempio come this-> Temp :: myVirtualFunction() - tale invocazione salta la risoluzione della tabella virtuale e la funzione dovrebbe essere inline senza problemi - perché e se tu 'D voglio farlo è un altro argomento :) – RnR

+5

@RnR. Non è necessario avere "questo->", basta usare il nome qualificato. E questo comportamento si verifica per i distruttori, i costruttori e, in generale, per gli operatori di assegnazione (vedi la mia risposta). –

0

Un compilatore può solo inline una funzione quando la chiamata può essere risolto in modo inequivocabile al momento della compilazione.

Le funzioni virtuali, tuttavia, vengono risolte in fase di esecuzione e pertanto il compilatore non può in linea la chiamata, poiché in fase di compilazione non è possibile determinare il tipo dinamico (e quindi l'implementazione della funzione da chiamare).

+1

Quando si chiama un metodo di classe base dalla stessa classe o derivata, la chiamata è non ambigua e non virtuale – sharptooth

+1

@sharptooth: ma sarebbe un metodo inline non virtuale. Il compilatore può incorporare funzioni a cui non lo chiedi e probabilmente sa meglio quando inline o no. Lascia che decida. –

+1

@dribeas: Sì, è esattamente quello di cui sto parlando. Ho solo obiettato all'affermazione che le finzioni virtuali sono risolte in fase di esecuzione - questo è vero solo quando la chiamata viene eseguita virtualmente, non per la classe esatta. – sharptooth

3

in linea in realtà non fa nulla - è un suggerimento. Il compilatore potrebbe ignorarlo o potrebbe incorporare un evento di chiamata senza in linea se vede l'implementazione e gli piace questa idea. Se è in gioco la chiarezza del codice, è necessario rimuovere in linea.

+2

Per i compilatori che operano solo su singole TU, possono solo incorporare funzioni implicitamente per le quali hanno la definizione. Una funzione può essere definita in più TU se la si rende in linea. 'inline' è più di un suggerimento e può avere un notevole miglioramento delle prestazioni per una build di g ++/makefile. –

1

Con i compilatori moderni, non farà alcun danno inli li. Alcune combo di compilatore/linker antiche potrebbero aver creato più vtables, ma non credo che questo sia più un problema.

1

Nei casi in cui la chiamata di funzione non è ambigua e la funzione è un candidato idoneo per l'inlining, il compilatore è abbastanza intelligente da inline comunque il codice.

Il resto del tempo "in linea virtuale" è un'assurdità, e in effetti alcuni compilatori non compilano tale codice.

+0

Quale versione di g ++ non compilerà virtual inline? –

+0

Hm. Il 4.1.1 che ho qui ora sembra essere felice. Ho incontrato per la prima volta problemi con questo codebase usando un 4.0.x. Indovina che le mie informazioni non sono aggiornate, modificate. – moonshadow

33

C'è una categoria di funzioni virtuali in cui ha ancora senso averle in linea. Si consideri il seguente caso:

class Base { 
public: 
    inline virtual ~Base() { } 
}; 

class Derived1 : public Base { 
    inline virtual ~Derived1() { } // Implicitly calls Base::~Base(); 
}; 

class Derived2 : public Derived1 { 
    inline virtual ~Derived2() { } // Implicitly calls Derived1::~Derived1(); 
}; 

void foo (Base * base) { 
    delete base;    // Virtual call 
} 

La chiamata a cancellare 'base', si esibirà una chiamata virtuale per chiamare corretta distruttore della classe derivata, questo invito non è inline. Tuttavia, poiché ogni distruttore chiama il suo distruttore principale (che in questi casi è vuoto), il compilatore può chiamare in linea quelle, poiché non chiamano virtualmente le funzioni della classe base.

Lo stesso principio esiste per costruttori della classe base o per qualsiasi insieme di funzioni in cui l'attuazione derivato richiede anche l'implementazione classi base.

+19

Si deve essere consapevoli che le parentesi vuote non significano sempre che il distruttore non fa nulla. I distruttori predefiniscono-distruggono ogni oggetto membro nella classe, quindi se nella classe base ci sono alcuni vettori che potrebbero essere un bel po 'di lavoro in quelle parentesi vuote! – Philip

+0

@Philip: buon punto. –

12

ho visto compilatori che non emettono alcun v-tavolo se nessuna funzione non inline affatto esiste (e definiti in un unico file di implementazione, invece di un colpo di testa poi). Avrebbero buttato errori come missing vtable-for-class-A o qualcosa di simile, e saresti stato confuso da morire, come lo ero io.

In effetti, questo non è conforme allo Standard, ma accade in questo caso considera di mettere almeno una funzione virtuale non nell'intestazione (se solo il distruttore virtuale), in modo che il compilatore possa emettere un vtable per la classe in quel luogo . So che succede con alcune versioni di gcc.

Come qualcuno ha detto, in linea funzioni virtuali possono essere un vantaggio volte, ma naturalmente il più delle volte si intende utilizzare quando si fanno non conoscere il tipo dinamico dell'oggetto, perché quello era l'unica ragione per virtual innanzitutto.

Il compilatore tuttavia non può ignorare completamente inline. Ha altre semantiche oltre ad accelerare una chiamata di funzione. Il implicita in linea per le definizioni in-classe è il meccanismo che permette di mettere la definizione nell'intestazione: Solo inline funzioni possono essere definiti più volte durante tutto il programma senza una violazione delle regole. Alla fine, si comporta come lo avresti definito solo una volta nell'intero programma, anche se hai incluso l'intestazione più volte in file diversi collegati insieme.

3

inline dichiarato funzioni virtuali sono inline quando viene chiamato attraverso oggetti e ignorato quando viene chiamato tramite puntatore o riferimenti.

10

Beh, in realtà funzioni virtuali possono sempre essere inline, a patto che stanno staticamente collegati tra loro: supponiamo di avere una classe astratta Base con una funzione virtuale F e classi derivate Derived1 e Derived2:

class Base { 
    virtual void F() = 0; 
}; 

class Derived1 : public Base { 
    virtual void F(); 
}; 

class Derived2 : public Base { 
    virtual void F(); 
}; 

Una chiamata ipotetico b->F(); (con b di tipo Base*) è ovviamente virtuale. Ma voi (o la compiler ...) potrebbe riscrivere in questo modo (supponiamo typeof è una funzione typeid -come che restituisce un valore che può essere utilizzato in un switch)

switch (typeof(b)) { 
    case Derived1: b->Derived1::F(); break; // static, inlineable call 
    case Derived2: b->Derived2::F(); break; // static, inlineable call 
    case Base:  assert(!"pure virtual function call!"); 
    default:  b->F(); break; // virtual call (dyn-loaded code) 
} 

mentre abbiamo ancora bisogno per la RTTI typeof, la chiamata può essere effettivamente integrata, basicamente, incorporando il vtable all'interno del flusso di istruzioni e specializzando la chiamata per tutte le classi coinvolte. Questo potrebbe essere generalizzata anche specializzandosi solo poche classi (diciamo, solo Derived1):

switch (typeof(b)) { 
    case Derived1: b->Derived1::F(); break; // hot path 
    default:  b->F(); break; // default virtual call, cold path 
} 
0

ha senso per rendere le funzioni virtuali e poi li chiamano su oggetti piuttosto che riferimenti o puntatori. Scott Meyer raccomanda, nel suo libro "C++ efficace", di non ridefinire mai una funzione non virtuale ereditata. Questo ha senso, perché quando si crea una classe con una funzione non virtuale e si ridefinisce la funzione in una classe derivata, si può essere certi di usarla correttamente da soli, ma non si può essere sicuri che altri la usino correttamente. Inoltre, puoi in seguito usarlo erroneamente.Quindi, se crei una funzione in una classe base e vuoi che sia modificabile, dovresti renderla virtuale. Se ha senso creare funzioni virtuali e chiamarle sugli oggetti, ha anche senso indicizzarle.

59

C++ 11 ha aggiunto final. Questo cambia la risposta accettata: non è più necessario conoscere la classe esatta dell'oggetto, è sufficiente conoscere l'oggetto ha almeno il tipo di classe in cui la funzione è stata dichiarata finale:

class A { 
    virtual void foo(); 
}; 
class B : public A { 
    inline virtual void foo() final { } 
}; 
class C : public B 
{ 
}; 

void bar(B const& b) { 
    A const& a = b; // Allowed, every B is an A. 
    a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C. 
} 
+0

Impossibile collegarlo in VS 2017. – Yola

+0

Non penso che funzioni in questo modo. L'invocazione di foo() attraverso un puntatore/riferimento di tipo A non può mai essere sottolineata. Chiamare b.foo() dovrebbe consentire l'inlining. A meno che tu non stia suggerendo che il compilatore sa già che questo è un tipo B perché è consapevole della linea precedente. Ma questo non è l'uso tipico. –

+0

Ad esempio, confronta il codice generato per la barra e qui: https://godbolt.org/g/xy3rNh –

Problemi correlati