Sind virtuelle Inline-Funktionen wirklich unsinnig?

172

Ich habe diese Frage erhalten, als ich einen Kommentar zur Codeüberprüfung erhielt, der besagt, dass virtuelle Funktionen nicht inline sein müssen.

Ich dachte, virtuelle Inline-Funktionen könnten in Szenarien nützlich sein, in denen Funktionen direkt für Objekte aufgerufen werden. Aber das Gegenargument kam mir in den Sinn: Warum sollte man virtuell definieren und dann Objekte verwenden, um Methoden aufzurufen?

Ist es am besten, keine virtuellen Inline-Funktionen zu verwenden, da diese sowieso fast nie erweitert werden?

Code-Snippet, das ich für die Analyse verwendet habe:

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;
}
aJ.
quelle
1
Überlegen Sie, ob Sie ein Beispiel mit den Schaltern kompilieren möchten, die Sie benötigen, um eine Assembler-Liste zu erhalten, und zeigen Sie dann dem Code-Prüfer, dass der Compiler tatsächlich virtuelle Funktionen einbinden kann.
Thomas L Holaday
1
Die oben genannten Punkte werden normalerweise nicht inline gesetzt, da Sie die virtuelle Funktion zugunsten der Basisklasse aufrufen. Obwohl es nur darauf ankommt, wie schlau der Compiler ist. Wenn es darauf hinweisen pTemp->myVirtualFunction()könnte , dass dies als nicht virtueller Anruf gelöst werden könnte, könnte es diesen Anruf inline haben. Dieser referenzierte Aufruf wird von g ++ 3.4.2 eingefügt: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();Ihr Code ist nicht.
Doc
1
Eine Sache, die gcc tatsächlich tut, ist, den vtable-Eintrag mit einem bestimmten Symbol zu vergleichen und dann eine inline-Variante in einer Schleife zu verwenden, wenn sie übereinstimmt. Dies ist besonders nützlich, wenn die Inline-Funktion leer ist und die Schleife in diesem Fall entfernt werden kann.
Simon Richter
1
@doc Moderner Compiler bemüht sich, zur Kompilierungszeit die möglichen Werte von Zeigern zu bestimmen. Die Verwendung eines Zeigers reicht nicht aus, um das Inlining auf einer signifikanten Optimierungsstufe zu verhindern. GCC führt sogar Vereinfachungen bei der Optimierung Null durch!
Neugieriger

Antworten:

153

Virtuelle Funktionen können manchmal eingebunden werden. Ein Auszug aus der ausgezeichneten C ++ - FAQ :

"Ein virtueller Inline-Aufruf kann nur dann eingebunden werden, wenn der Compiler die" genaue Klasse "des Objekts kennt, das das Ziel des virtuellen Funktionsaufrufs ist. Dies kann nur geschehen, wenn der Compiler ein tatsächliches Objekt anstelle eines Zeigers oder hat Verweis auf ein Objekt. Das heißt, entweder mit einem lokalen Objekt, einem globalen / statischen Objekt oder einem vollständig enthaltenen Objekt in einem Verbund. "

ya23
quelle
7
Es stimmt, aber es ist zu beachten, dass der Compiler den Inline-Bezeichner auch dann ignorieren kann, wenn der Aufruf zur Kompilierungszeit aufgelöst und inline ausgeführt werden kann.
Scharfzahn
6
Eine andere Situation, in der ich denke, dass Inlining passieren kann, ist, wenn Sie die Methode beispielsweise als this-> Temp :: myVirtualFunction () aufrufen - ein solcher Aufruf überspringt die Auflösung der virtuellen Tabelle und die Funktion sollte ohne Probleme inlined werden - warum und ob Sie '
Ich
5
@RnR. Es ist nicht notwendig, 'this->' zu haben, es reicht aus, nur den qualifizierten Namen zu verwenden. Und dieses Verhalten tritt für Destruktoren, Konstruktoren und allgemein für Zuweisungsoperatoren auf (siehe meine Antwort).
Richard Corden
2
sharptooth - true, aber AFAIK gilt dies für alle Inline-Funktionen, nicht nur für virtuelle Inline-Funktionen.
Colen
2
void f (const Base & lhs, const Base & rhs) {} ------ Bei der Implementierung der Funktion wissen Sie bis zur Laufzeit nie, auf was lhs und rhs zeigen.
Baiyan Huang
72

C ++ 11 wurde hinzugefügt final. Dies ändert die akzeptierte Antwort: Es ist nicht mehr erforderlich, die genaue Klasse des Objekts zu kennen. Es reicht aus zu wissen, dass das Objekt mindestens den Klassentyp hat, in dem die Funktion als endgültig deklariert wurde:

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.
}
MSalters
quelle
Konnte es in VS 2017 nicht inline.
Yola
1
Ich denke nicht, dass es so funktioniert. Der Aufruf von foo () über einen Zeiger / eine Referenz vom Typ A kann niemals inline erfolgen. Das Aufrufen von b.foo () sollte Inlining ermöglichen. Es sei denn, Sie schlagen vor, dass der Compiler bereits weiß, dass dies ein Typ B ist, da er die vorherige Zeile kennt. Aber das ist nicht die typische Verwendung.
Jeffrey Faust
Vergleichen Sie zum Beispiel den generierten Code für bar und bas hier: godbolt.org/g/xy3rNh
Jeffrey Faust
@ JeffreyFaust Es gibt keinen Grund, warum Informationen nicht verbreitet werden sollten, oder? Und iccscheint es laut diesem Link zu tun.
Alexey Romanov
@AlexeyRomanov Compiler haben die Freiheit, über den Standard hinaus zu optimieren, und das tun sie auf jeden Fall! In einfachen Fällen wie oben könnte der Compiler den Typ kennen und diese Optimierung durchführen. Die Dinge sind selten so einfach und es ist nicht typisch, den tatsächlichen Typ einer polymorphen Variablen zur Kompilierungszeit bestimmen zu können. Ich denke, OP kümmert sich um "im Allgemeinen" und nicht um diese Sonderfälle.
Jeffrey Faust
37

Es gibt eine Kategorie von virtuellen Funktionen, bei denen es immer noch sinnvoll ist, sie inline zu haben. Betrachten Sie den folgenden Fall:

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
}

Der Aufruf zum Löschen von 'base' führt einen virtuellen Aufruf zum Aufrufen des korrekten abgeleiteten Klassendestruktors durch. Dieser Aufruf ist nicht inline. Da jedoch jeder Destruktor seinen übergeordneten Destruktor aufruft (der in diesen Fällen leer ist), kann der Compiler diese Aufrufe einbinden, da sie die Basisklassenfunktionen nicht virtuell aufrufen.

Das gleiche Prinzip gilt für Basisklassenkonstruktoren oder für alle Funktionen, bei denen die abgeleitete Implementierung auch die Basisklassenimplementierung aufruft.

Richard Corden
quelle
23
Man sollte sich jedoch bewusst sein, dass leere Klammern nicht immer bedeuten, dass der Destruktor nichts tut. Destruktoren zerstören standardmäßig jedes Mitgliedsobjekt in der Klasse. Wenn Sie also einige Vektoren in der Basisklasse haben, kann dies in diesen leeren Klammern eine Menge Arbeit bedeuten!
Philip
14

Ich habe Compiler gesehen, die keine V-Tabelle ausgeben, wenn überhaupt keine Nicht-Inline-Funktion vorhanden ist (und dann in einer Implementierungsdatei anstelle eines Headers definiert ist). Sie würden Fehler wie missing vtable-for-class-Aoder ähnliches werfen , und Sie würden höllisch verwirrt sein, wie ich es war.

Dies entspricht zwar nicht dem Standard, es kommt jedoch vor, dass mindestens eine virtuelle Funktion nicht in den Header eingefügt wird (wenn nur der virtuelle Destruktor), damit der Compiler an dieser Stelle eine vtable für die Klasse ausgeben kann. Ich weiß, dass es mit einigen Versionen von passiert gcc.

Wie bereits erwähnt, können virtuelle Inline-Funktionen manchmal von Vorteil sein , aber natürlich werden Sie sie meistens verwenden, wenn Sie den dynamischen Typ des Objekts nicht kennen, da dies virtualin erster Linie der ganze Grund dafür war .

Der Compiler kann dies jedoch nicht vollständig ignorieren inline. Es hat eine andere Semantik als die Beschleunigung eines Funktionsaufrufs. Die implizite Inline für klasseninterne Definitionen ist der Mechanismus, mit dem Sie die Definition in den Header einfügen können: Nur inlineFunktionen können im gesamten Programm mehrmals definiert werden, ohne dass Regeln verletzt werden. Am Ende verhält es sich so, als hätten Sie es im gesamten Programm nur einmal definiert, obwohl Sie den Header mehrmals in verschiedene miteinander verknüpfte Dateien eingefügt haben.

Johannes Schaub - litb
quelle
11

Nun, tatsächlich können virtuelle Funktionen immer inline sein , solange sie statisch miteinander verbunden sind: Nehmen wir an, wir haben eine abstrakte Klasse Base mit einer virtuellen Funktion Fund abgeleiteten Klassen Derived1und Derived2:

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

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

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

Ein hypotetischer Anruf b->F();(mit bTyp Base*) ist offensichtlich virtuell. Aber Sie (oder der Compiler ...) könnten es so umschreiben (nehmen wir an, es typeofhandelt sich um eine typeidähnliche Funktion, die einen Wert zurückgibt, der in a verwendet werden kann 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)
}

Während wir noch RTTI für das benötigen typeof, kann der Aufruf effektiv eingebunden werden, indem im Grunde die vtable in den Anweisungsstrom eingebettet und der Aufruf für alle beteiligten Klassen spezialisiert wird. Dies könnte auch verallgemeinert werden, indem nur wenige Klassen (z. B. nur Derived1) spezialisiert werden:

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // hot path
  default:       b->F(); break; // default virtual call, cold path
}
CAFxX
quelle
Sind sie Compiler, die dies tun? Oder ist das nur Spekulation? Es tut mir leid, wenn ich zu skeptisch bin, aber Ihr Ton in der obigen Beschreibung klingt wie "sie könnten das total!", Was sich von "einige Compiler machen das" unterscheidet.
Alex Meiburg
Ja, Graal macht polymorphes Inlining (auch für LLVM-Bitcode über Sulong)
CAFxX
3

Inline macht wirklich nichts - es ist ein Hinweis. Der Compiler ignoriert es möglicherweise oder integriert ein Aufrufereignis ohne Inline, wenn er die Implementierung sieht und diese Idee mag. Wenn es um Codeklarheit geht, sollte die Inline entfernt werden.

scharfer Zahn
quelle
2
Für Compiler, die nur mit einzelnen TUs arbeiten, können sie nur implizite Funktionen inline, für die sie die Definition haben. Eine Funktion kann nur in mehreren TUs definiert werden, wenn Sie sie inline machen. 'inline' ist mehr als ein Hinweis und kann eine dramatische Leistungsverbesserung für einen g ++ / makefile-Build bewirken.
Richard Corden
3

Inlined deklarierte virtuelle Funktionen werden beim Aufruf über Objekte inlined und beim Aufruf über Zeiger oder Referenzen ignoriert.

Tarachandverma
quelle
1

Mit modernen Compilern schadet es nicht, sie einzuschleusen. Einige alte Compiler / Linker-Kombinationen haben möglicherweise mehrere vtables erstellt, aber ich glaube nicht, dass dies ein Problem mehr ist.


quelle
1

Ein Compiler kann eine Funktion nur dann inline setzen, wenn der Aufruf zur Kompilierungszeit eindeutig aufgelöst werden kann.

Virtuelle Funktionen werden jedoch zur Laufzeit aufgelöst, sodass der Compiler den Aufruf nicht einbinden kann, da beim Kompilierungstyp der dynamische Typ (und damit die aufzurufende Funktionsimplementierung) nicht bestimmt werden kann.

PaulJWilliams
quelle
1
Wenn Sie eine Basisklassenmethode aus derselben oder einer abgeleiteten Klasse aufrufen, ist der Aufruf eindeutig und nicht virtuell
Sharptooth
1
@sharptooth: aber dann wäre es eine nicht virtuelle Inline-Methode. Der Compiler kann Inline-Funktionen ausführen, nach denen Sie nicht gefragt werden, und er weiß wahrscheinlich besser, wann Inline-Funktionen ausgeführt werden sollen oder nicht. Lass es entscheiden.
David Rodríguez - Dribeas
1
@dribeas: Ja, genau darüber spreche ich. Ich habe nur gegen die Aussage protestiert, dass virtuelle Funktionen zur Laufzeit aufgelöst werden - dies gilt nur, wenn der Aufruf virtuell erfolgt, nicht für die genaue Klasse.
Scharfzahn
Ich glaube das ist Unsinn. Jede Funktion kann immer inline sein, egal wie groß sie ist oder ob sie virtuell ist oder nicht. Dies hängt davon ab, wie der Compiler geschrieben wurde. Wenn Sie nicht einverstanden sind, gehe ich davon aus, dass Ihr Compiler auch keinen nicht inlinierten Code erzeugen kann. Das heißt: Der Compiler kann Code enthalten, der zur Laufzeit die Bedingungen testet, die er zur Kompilierungszeit nicht auflösen konnte. Es ist so, als ob die modernen Compiler konstante Werte auflösen / nummerische Ausdrücke zur Kompilierungszeit reduzieren können. Wenn eine Funktion / Methode nicht inline ist, bedeutet dies nicht, dass sie nicht inliniert werden kann.
1

In den Fällen, in denen der Funktionsaufruf eindeutig ist und die Funktion ein geeigneter Kandidat für das Inlining ist, ist der Compiler intelligent genug, um den Code trotzdem zu inlineieren.

Der Rest der Zeit "Inline Virtual" ist ein Unsinn, und tatsächlich kompilieren einige Compiler diesen Code nicht.

Mondschatten
quelle
Welche Version von g ++ kompiliert keine Inline-Virtuals?
Thomas L Holaday
Hm. Die 4.1.1, die ich hier habe, scheint jetzt glücklich zu sein. Ich habe zum ersten Mal Probleme mit dieser Codebasis unter Verwendung einer 4.0.x festgestellt. Ich denke, meine Informationen sind veraltet und wurden bearbeitet.
Mondschatten
0

Es ist sinnvoll, virtuelle Funktionen zu erstellen und sie dann für Objekte anstatt für Referenzen oder Zeiger aufzurufen. Scott Meyer empfiehlt in seinem Buch "effektives c ++", eine geerbte nicht virtuelle Funktion niemals neu zu definieren. Das ist sinnvoll, denn wenn Sie eine Klasse mit einer nicht virtuellen Funktion erstellen und die Funktion in einer abgeleiteten Klasse neu definieren, können Sie sicher sein, dass Sie sie selbst richtig verwenden, aber Sie können nicht sicher sein, dass andere sie richtig verwenden. Sie können es auch zu einem späteren Zeitpunkt falsch verwenden. Wenn Sie also eine Funktion in einer Basisklasse erstellen und möchten, dass sie erneut definiert werden kann, sollten Sie sie virtuell machen. Wenn es sinnvoll ist, virtuelle Funktionen zu erstellen und sie für Objekte aufzurufen, ist es auch sinnvoll, sie zu integrieren.

Balthazar
quelle
0

In einigen Fällen kann das Hinzufügen von "Inline" zu einer virtuellen endgültigen Überschreibung dazu führen, dass Ihr Code nicht kompiliert wird, sodass manchmal ein Unterschied besteht (zumindest unter dem VS2017-Compiler)!

Tatsächlich habe ich in VS2017 eine virtuelle Inline-Funktion zum endgültigen Überschreiben ausgeführt und den C ++ 17-Standard zum Kompilieren und Verknüpfen hinzugefügt. Aus irgendeinem Grund ist dies fehlgeschlagen, wenn ich zwei Projekte verwende.

Ich hatte ein Testprojekt und eine Implementierungs-DLL, die ich als Unit-Test durchführe. Im Testprojekt habe ich eine "linker_includes.cpp" -Datei, die die * .cpp-Dateien aus dem anderen Projekt enthält, die benötigt werden. Ich weiß ... Ich weiß, dass ich msbuild für die Verwendung der Objektdateien aus der DLL einrichten kann, aber bitte beachten Sie, dass es sich um eine Microsoft-spezifische Lösung handelt, während das Einschließen der CPP-Dateien nicht mit dem Build-System zusammenhängt und viel einfacher zu versionieren ist eine CPP-Datei als XML-Dateien und Projekteinstellungen und so ...

Interessant war, dass ich ständig Linkerfehler aus dem Testprojekt bekam. Auch wenn ich die Definition der fehlenden Funktionen durch Kopieren, Einfügen und nicht durch Einschließen hinzugefügt habe! So seltsam. Das andere Projekt wurde erstellt und es gibt keine Verbindung zwischen den beiden, außer das Markieren einer Projektreferenz. Es gibt also eine Erstellungsreihenfolge, um sicherzustellen, dass beide immer erstellt werden ...

Ich denke, es ist eine Art Fehler im Compiler. Ich habe keine Ahnung, ob es in dem mit VS2020 gelieferten Compiler vorhanden ist, da ich eine ältere Version verwende, da einige SDKs nur damit ordnungsgemäß funktionieren :-(

Ich wollte nur hinzufügen, dass nicht nur das Markieren als Inline etwas bedeuten kann, sondern unter bestimmten Umständen sogar dazu führen kann, dass Ihr Code nicht erstellt wird! Das ist komisch, aber gut zu wissen.

PS.: Der Code, an dem ich arbeite, bezieht sich auf Computergrafiken, daher bevorzuge ich Inlining. Deshalb habe ich sowohl final als auch inline verwendet. Ich habe den endgültigen Spezifizierer beibehalten, um zu hoffen, dass der Release-Build intelligent genug ist, um die DLL durch Inlining zu erstellen, auch ohne dass ich dies direkt andeutete ...

PS (Linux).: Ich gehe davon aus, dass in gcc oder clang nicht dasselbe passiert, wie ich es routinemäßig getan habe. Ich bin mir nicht sicher, woher dieses Problem kommt ... Ich bevorzuge C ++ unter Linux oder zumindest mit etwas gcc, aber manchmal unterscheidet sich das Projekt in den Anforderungen.

Prenex
quelle