Le funzioni virtuali inline sono davvero un non-senso?


172

Ho ricevuto questa domanda quando ho ricevuto un commento di revisione del codice che diceva che le funzioni virtuali non dovevano essere in linea.

Ho pensato che le funzioni virtuali inline possano tornare utili in scenari in cui le funzioni vengono chiamate direttamente sugli oggetti. Ma la controargomentazione mi è venuta in mente è: perché si dovrebbe definire virtuale e quindi usare oggetti per chiamare metodi?

È meglio non usare le funzioni virtuali inline, dato che non sono quasi mai espanse comunque?

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;
}

1
Prendi in considerazione la possibilità di compilare un esempio con qualsiasi switch necessario per ottenere un elenco di assemblatori e quindi mostrare al revisore del codice che, in effetti, il compilatore può incorporare funzioni virtuali.
Thomas L Holaday,

1
Quanto sopra di solito non sarà in linea, perché stai chiamando la funzione virtuale in aiuto della classe base. Anche se dipende solo da quanto sia intelligente il compilatore. Se fosse in grado di sottolineare che pTemp->myVirtualFunction()potrebbe essere risolto come chiamata non virtuale, potrebbe essere inline quella chiamata. Questa chiamata referenziata è sottolineata da g ++ 3.4.2: il TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();tuo codice non lo è.
doc

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

1
@doc Il compilatore moderno si impegna a determinare in fase di compilazione i possibili valori dei puntatori. Il solo utilizzo di un puntatore non è sufficiente per impedire l'allineamento a qualsiasi livello di ottimizzazione significativo; GCC esegue anche semplificazioni a zero di ottimizzazione!
curiousguy,

Risposte:


153

Le funzioni virtuali possono essere incorporate a volte. Un estratto dall'eccellente faq C ++ :

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


7
È vero, ma vale la pena ricordare che il compilatore è libero di ignorare l'identificatore in linea anche se la chiamata può essere risolta al momento della compilazione e può essere incorporata.
sharptooth,

6
Un'altra situazione in cui penso che possa verificarsi l'allineamento è quando chiameresti il ​​metodo come questo-> Temp :: myVirtualFunction () - tale invocazione salta la risoluzione della tabella virtuale e la funzione dovrebbe essere incorporata senza problemi - perché e se tu ' Vorrei farlo è un altro argomento :)
RnR

5
@RnR. Non è necessario avere 'this->', basta usare il nome qualificato. E questo comportamento si verifica per distruttori, costruttori e in generale per operatori di assegnazione (vedi la mia risposta).
Richard Corden,

2
sharptooth: vero, ma AFAIK questo vale per tutte le funzioni inline, non solo per le funzioni inline virtuali.
Colen,

2
void f (const Base & lhs, const Base & rhs) {} ------ Nell'implementazione della funzione, non si sa mai a cosa puntano lhs e rhs fino al runtime.
Baiyan Huang,

72

C ++ 11 è stato aggiunto final. Questo cambia la risposta accettata: non è più necessario conoscere la classe esatta dell'oggetto, è sufficiente sapere che 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.
}

Non sono riuscito a integrarlo in VS 2017.
Yola,

1
Non penso che funzioni in questo modo. L'invocazione di foo () attraverso un puntatore / riferimento di tipo A non può mai essere incorporata. La chiamata a b.foo () dovrebbe consentire l'allineamento. A meno che tu non stia suggerendo che il compilatore sappia già che si tratta di un tipo B perché è a conoscenza della riga precedente. Ma questo non è l'uso tipico.
Jeffrey Faust,

Ad esempio, confronta il codice generato per bar e bas qui: godbolt.org/g/xy3rNh
Jeffrey Faust,

@JeffreyFaust Non c'è motivo per cui le informazioni non debbano essere propagate, vero? E iccsembra farlo, secondo quel link.
Alexey Romanov,

I compilatori @AlexeyRomanov hanno la libertà di ottimizzare oltre lo standard, e certamente lo fanno! Per casi semplici come sopra, il compilatore potrebbe conoscere il tipo e fare questa ottimizzazione. Le cose raramente sono così semplici e non è tipico riuscire a determinare il tipo effettivo di una variabile polimorfica al momento della compilazione. Penso che a OP importi "in generale" e non per questi casi speciali.
Jeffrey Faust,

37

Esiste una categoria di funzioni virtuali in cui ha ancora senso averle in linea. Considera il caso seguente:

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 per eliminare 'base', eseguirà una chiamata virtuale per chiamare il distruttore di classe derivato corretto, questa chiamata non è inline. Tuttavia, poiché ogni distruttore chiama il suo genitore distruttore (che in questi casi è vuoto), il compilatore può incorporare quelli chiamate, poiché non chiamano virtualmente le funzioni della classe base.

Lo stesso principio esiste per i costruttori di classi base o per qualsiasi set di funzioni in cui l'implementazione derivata chiama anche l'implementazione delle classi base.


23
Bisogna essere consapevoli del fatto che le parentesi graffe vuote non significano sempre che il distruttore non fa nulla. I distruttori distruggono per impostazione predefinita ogni oggetto membro nella classe, quindi se nella classe base ci sono alcuni vettori che potrebbero essere parecchio lavoro in quelle parentesi vuote!
Filippo,

14

Ho visto compilatori che non emettono alcuna v-table se non esiste alcuna funzione non inline (e definita in un file di implementazione anziché in un'intestazione). Avrebbero lanciato errori comemissing vtable-for-class-A o qualcosa di simile, e tu saresti confuso da morire, come me.

In effetti, questo non è conforme allo Standard, ma succede quindi considera di non inserire almeno una funzione virtuale nell'intestazione (se solo il distruttore virtuale), in modo che il compilatore possa emettere una tabella per la classe in quel posto. So che succede con alcune versioni digcc .

Come qualcuno ha detto, le funzioni virtuali inline possono essere un vantaggio a volte , ma ovviamente molto spesso lo userete quando non conoscete il tipo dinamico dell'oggetto, perché questa è stata la ragione principale virtualin primo luogo.

Il compilatore tuttavia non può ignorare completamente inline. Ha altra semantica oltre a velocizzare una chiamata di funzione. L' inline implicito per le definizioni in classe è il meccanismo che consente di inserire la definizione nell'intestazione: solo le inlinefunzioni possono essere definite più volte in tutto il programma senza violare alcuna regola. Alla fine, si comporta come l'avresti definito una sola volta nell'intero programma, anche se hai incluso più volte l'intestazione in file diversi collegati tra loro.


11

Bene, in realtà le funzioni virtuali possono sempre essere integrate , purché siano staticamente collegate tra loro: supponiamo di avere una classe astratta Base con una funzione virtuale Fe classi derivate Derived1e Derived2:

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

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

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

Una chiamata ipotetica b->F();(con btipo Base*) è ovviamente virtuale. Ma tu (o il compilatore ...) potresti riscriverlo in questo modo (supponiamo che typeofsia una typeidfunzione simile a quella che restituisce un valore che può essere usato in a 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 di RTTI per il typeof, la chiamata può essere efficacemente incorporata, fondamentalmente, incorporando la vtable all'interno del flusso di istruzioni e specializzando la chiamata per tutte le classi coinvolte. Questo potrebbe anche essere generalizzato specializzando solo alcune classi (diciamo, solo Derived1):

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

Sono dei compilatori che lo fanno? O è solo una speculazione? Scusate se sono eccessivamente scettico, ma il vostro tono nella descrizione sopra suona un po 'come - "potrebbero farlo totalmente!", Che è diverso da "alcuni compilatori lo fanno".
Alex Meiburg,

Sì, Graal esegue il rivestimento polimorfico (anche per bitcode LLVM tramite Sulong)
CAFxX


3

inline non fa davvero nulla - è un suggerimento. Il compilatore potrebbe ignorarlo o potrebbe incorporare un evento call senza inline se vede l'implementazione e gradisce questa idea. Se è in gioco la chiarezza del codice, l' inline dovrebbe essere rimosso.


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

3

Le funzioni virtuali dichiarate incorporate vengono incorporate quando richiamate attraverso oggetti e ignorate quando richiamate tramite puntatore o riferimenti.


1

Con i compilatori moderni, non farà alcun male a loro. Alcune combo di compilatori / linker antichi potrebbero aver creato più vtables, ma non credo più che sia un problema.


1

Un compilatore può incorporare una funzione solo quando la chiamata può essere risolta in modo univoco al momento della compilazione.

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


1
Quando chiami un metodo della classe base dalla stessa classe derivata o la chiamata è inequivocabile e non virtuale
sharptooth,

1
@sharptooth: ma sarebbe un metodo inline non virtuale. Il compilatore può incorporare funzioni alle quali non gli viene chiesto, e probabilmente sa meglio quando incorporare o meno. Lascia che decida.
David Rodríguez - dribeas,

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

Credo che sia una sciocchezza. Qualsiasi funzione può sempre essere incorporata, non importa quanto sia grande o se sia virtuale o meno. Dipende da come è stato scritto il compilatore. Se non sei d'accordo, mi aspetto che il tuo compilatore non sia in grado di produrre codice non incorporato. Cioè: il compilatore può includere codice che in fase di runtime verifica le condizioni che non è stato possibile risolvere in fase di compilazione. È proprio come i compilatori moderni possono risolvere valori costanti / ridurre le espressioni numeriche in fase di compilazione. Se una funzione / metodo non è inline, ciò non significa che non possa essere inline.

1

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

Il resto del tempo "inline virtual" è un'assurdità, e in effetti alcuni compilatori non compileranno quel codice.


Quale versione di g ++ non compila virtual inline?
Thomas L Holaday,

Hm. Il 4.1.1 che ho qui ora sembra essere felice. Ho riscontrato per la prima volta problemi con questo codice utilizzando un 4.0.x. Indovina che le mie informazioni non sono aggiornate, modificate.
Moonshadow

0

Ha senso creare funzioni virtuali e quindi chiamarle su oggetti anziché su riferimenti o puntatori. Scott Meyer raccomanda, nel suo libro "c ++ efficace", di non ridefinire mai una funzione non virtuale ereditata. Ciò 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 sicuri di utilizzarla correttamente da soli, ma non si può essere sicuri che altri la utilizzeranno correttamente. Inoltre, in un secondo momento è possibile utilizzarlo in modo errato. Quindi, se si crea una funzione in una classe base e si desidera che sia ridifinabile, è necessario renderla virtuale. Se ha senso creare funzioni virtuali e chiamarle su oggetti, ha anche senso incorporarle.


0

In realtà in alcuni casi l'aggiunta di "inline" a un override finale virtuale può impedire la compilazione del codice, quindi a volte c'è una differenza (almeno nel compilatore VS2017s)!

In realtà stavo facendo una funzione di override finale virtuale inline in VS2017 aggiungendo lo standard c ++ 17 per la compilazione e il collegamento e per qualche ragione non è riuscito quando sto usando due progetti.

Ho avuto un progetto di test e una DLL di implementazione che sto testando l'unità. Nel progetto di test sto avendo un file "linker_includes.cpp" che # include i file * .cpp dell'altro progetto necessari. Lo so ... So che posso configurare msbuild per usare i file oggetto dalla DLL, ma tieni presente che si tratta di una soluzione specifica per Microsoft, mentre includere i file cpp non è correlato al sistema di compilazione e molto più facile alla versione un file cpp rispetto ai file xml e alle impostazioni del progetto e simili ...

La cosa interessante è che stavo ricevendo costantemente errori del linker dal progetto di test. Anche se ho aggiunto la definizione delle funzioni mancanti con copia incolla e non tramite include! Così strano. L'altro progetto è stato creato e non esiste alcuna connessione tra i due oltre a contrassegnare un riferimento al progetto, quindi esiste un ordine di costruzione per garantire che entrambi siano sempre creati ...

Penso che sia una specie di bug nel compilatore. Non ho idea se esiste nel compilatore fornito con VS2020, perché sto usando una versione precedente perché alcuni SDK funzionano correttamente solo con quello :-(

Volevo solo aggiungere che non solo contrassegnarli come in linea può significare qualcosa, ma potrebbe anche rendere il tuo codice non compilato in alcune rare circostanze! Questo è strano, ma buono a sapersi.

PS .: Il codice su cui sto lavorando è legato alla grafica del computer, quindi preferisco inline ed è per questo che ho usato sia final che inline. Ho mantenuto l'identificatore finale sperando che la build di rilascio sia abbastanza intelligente da creare la DLL incorporandola anche senza di me suggerendo direttamente quindi ...

PS (Linux) .: Mi aspetto che lo stesso non accada in gcc o clang, come facevo abitualmente per fare questo tipo di cose. Non sono sicuro da dove provenga questo problema ... Preferisco fare c ++ su Linux o almeno con qualche gcc, ma a volte il progetto ha esigenze diverse.

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.