Sind inline virtuelle functionen wirklich ein Unsinn?

Ich habe diese Frage erhalten, als ich einen Code-Review-Kommentar erhielt, in dem es hieß, virtuelle functionen müssten nicht inline sein.

Ich dachte, inline virtuelle functionen könnten in Szenarien nützlich sein, in denen functionen direkt an Objekten 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, inline virtuelle functionen nicht zu verwenden, da sie ohnehin 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"<myVirtualFunction(); return 0; } 

   

Virtuelle functionen können manchmal inline sein. Ein Auszug aus der ausgezeichneten C ++ – FAQ :

“Der einzige Zeitpunkt, an dem ein Inline-Aufruf ausgeführt werden kann, ist, wenn der Compiler die” Exakte class “des Objekts kennt, das das Ziel des Aufrufs der virtuellen function ist. Dies kann nur passieren, wenn der Compiler ein tatsächliches Objekt und nicht einen pointers oder hat Verweis auf ein Objekt, dh entweder mit einem lokalen Objekt, einem globalen / statischen Objekt oder einem vollständig enthaltenen Objekt in einem Verbund. ”

C ++ 11 hat das final hinzugefügt. Dies ändert die akzeptierte Antwort: Es ist nicht länger notwendig, die genaue class des Objekts zu kennen, es genügt zu wissen, dass das Objekt mindestens den classntyp aufweist, in dem die function 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. } 

Es gibt eine Kategorie von virtuellen functionen, bei der 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 aus, um den korrekten abgeleiteten classndestruktor aufzurufen. Dieser Aufruf ist nicht inlined. Da jedoch jeder Destruktor seinen übergeordneten Destruktor aufruft (der in diesen Fällen leer ist), kann der Compiler diese Aufrufe inline ausführen, da sie die Basisklassenfunktionen nicht virtuell aufrufen.

Das gleiche Prinzip gibt es für Basisklassenkonstruktoren oder für jeden Satz von functionen, bei denen die abgeleitete Implementierung auch die Basisklassenimplementierung aufruft.

Ich habe Compiler gesehen, die keine v-Tabelle ausgeben, wenn überhaupt keine Nicht-Inline-function existiert (und in einer Implementationsdatei anstatt einer Überschrift definiert ist). Sie würden Fehler wie missing vtable-for-class-A oder etwas Ähnliches casting, und Sie würden wie die Hölle verwirrt sein, so wie ich war.

In der Tat stimmt das nicht mit dem Standard überein, aber es wird in Betracht gezogen, dass mindestens eine virtuelle function nicht im Header (wenn nur der virtuelle Destruktor) platziert wird, damit der Compiler an dieser Stelle eine vtable für die class ausgeben kann. Ich weiß, dass es mit einigen Versionen von gcc passiert.

Wie bereits erwähnt, können inline virtuelle functionen manchmal von Vorteil sein, aber natürlich werden Sie es am häufigsten verwenden, wenn Sie den dynamischen Typ des Objekts nicht kennen, denn das war der eigentliche Grund für virtual .

Der Compiler kann jedoch nicht vollständig inline ignorieren. Es hat andere Semantiken abgesehen von der Beschleunigung eines functionsaufrufs. Der implizite InlineCode für In-Class-Definitionen ist der Mechanismus, mit dem Sie die Definition in den Header inline können: Nur inline functionen können mehrfach im gesamten Programm definiert werden, ohne dass Regeln verletzt werden. Am Ende verhält es sich so, wie Sie es nur einmal im ganzen Programm definiert hätten, obwohl Sie die Kopfzeile mehrfach in verschiedene miteinander verknüpfte Dateien eingefügt haben.

Nun, eigentlich können virtuelle functionen immer inline sein , solange sie statisch miteinander verknüpft sind: Angenommen, wir haben eine abstrakte class Base mit einer virtuellen function F und den abgeleiteten classn Derived1 und Derived2 :

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

Ein hypothetischer Aufruf b->F(); (mit b vom Typ Base* ) ist offensichtlich virtuell. Aber Sie (oder der Compiler …) könnten es so umschreiben (nehmen wir an, dass typeof eine typeid ähnliche function ist, die einen Wert zurückgibt, der in einem 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 den Typ von benötigen, kann der Aufruf effektiv eingegliedert werden, indem im Grunde die vtable in den Befehlsstrom eingebettet wird und der Aufruf für alle beteiligten classn spezialisiert wird. Dies könnte auch verallgemeinert werden, indem nur einige wenige classn spezialisiert werden (zB nur Derived1 ):

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

Inline tut wirklich nichts – es ist ein Hinweis. Der Compiler ignoriert sie möglicherweise oder fügt ein Aufrufereignis ohne Inline ein, wenn die Implementierung erkannt wird und diese Idee gefällt. Wenn Code-Klarheit auf dem Spiel steht, sollte das Inline entfernt werden.

Inline deklarierte virtuelle functionen werden beim Aufruf durch Objekte inline, beim Aufruf über pointers oder Referenzen ignoriert.

Eine virtuelle Methode inline zu markieren, hilft bei der weiteren Optimierung virtueller functionen in zwei Fällen:

Mit modernen Compilern wird es nicht schaden, sie zu nutzen. Einige alte Compiler / Linker Combos haben möglicherweise mehrere VTables erstellt, aber ich glaube nicht, dass das ein Problem mehr ist.

In den Fällen, in denen der functionsaufruf eindeutig ist und die function ein geeigneter Kandidat für Inlining ist, ist der Compiler intelligent genug, um den Code trotzdem zu inline zu schreiben.

Der Rest der Zeit “Inline-Virtual” ist ein Unsinn, und in der Tat einige Compiler werden diesen Code nicht kompilieren.

Ein Compiler kann nur dann eine function inline ausführen, wenn der Aufruf zur Kompilierzeit eindeutig aufgetriggers werden kann.

Virtuelle functionen werden jedoch zur Laufzeit aufgetriggers, so dass der Compiler den Aufruf nicht inline machen kann, da beim Kompilieren der dynamische Typ (und damit die aufzurufende functionsimplementierung) nicht ermittelt werden kann.

Es macht Sinn, virtuelle functionen zu erstellen und sie dann auf Objekten anstatt auf Referenzen oder pointersn aufzurufen. Scott Meyer empfiehlt in seinem Buch “effective c ++”, eine geerbte nicht-virtuelle function nie neu zu definieren. Das macht Sinn, denn wenn Sie eine class mit einer nicht virtuellen function erstellen und die function in einer abgeleiteten class neu definieren, können Sie sicher sein, dass Sie sie selbst korrekt verwenden, aber Sie können nicht sicher sein, dass andere sie korrekt verwenden. Sie können es zu einem späteren Zeitpunkt auch falsch verwenden. Wenn Sie also eine function in einer Basisklasse erstellen und diese umdefinierbar machen möchten, sollten Sie sie virtuell machen. Wenn es sinnvoll ist, virtuelle functionen zu erstellen und diese auf Objekten aufzurufen, ist es auch sinnvoll, sie zu inline zu schreiben.