2016-04-11 6 views
6

Stellen Sie sich die folgenden Klassen:Wird eine Methode virtuell aufgerufen, auch wenn keine Unterklasse sie überschreibt?

class A { 
    public: 
    virtual void print()   { printf("A\n"); } 
}; 
class B : public A { 
    public: 
    virtual void print() override { printf("B\n"); } 
}; 
class C : public B { 
    // no override of print 
}; 

Und jetzt, wenn Sie eine Instanz von B erstellen und drucken nennen:

B * b = new B; 
b->print(); 

Wird diese Methode praktisch genannt werden? Mit anderen Worten, wird die genaue Methode, die aufgerufen werden soll, zur Kompilierungszeit oder zur Laufzeit bestimmt?

Theoretisch kann es zur Kompilierzeit bestimmt werden, weil wir wissen, dass keine der Unterklassen von B diese Methode außer Kraft setzt, also unabhängig davon, was ich dem Zeiger auf B zuteile, wird es immer B::print() aufrufen.

Weiß der Compiler es auch und rette mich vor unnötigem Overhead des virtuellen Anrufs?

+1

Welcher ist der "Compiler"? –

+2

Wahrscheinlich implementierungsspezifisch - einige, aber nicht alle Compiler würden die von Ihnen vorgeschlagene Optimierung durchführen. –

+1

Höchstwahrscheinlich wird es optimiert, wenn es zusätzlich markiert ist (http://en.cppreference.com/w/cpp/language/final), aber ich kann das nicht mit Sicherheit sagen, da ich es nicht weiß viel über den Mut der Compiler. – vsoftco

Antwort

3

Theoretisch kann es bei der Kompilierung-Zeit bestimmt werden, weil wir wissen, dass keiner der Unterklassen von B überschreibt diese Methode

Sie können dies im allgemeinen Fall bei der Kompilierung nicht bestimmen, weil C++ Der Compiler beschäftigt sich jeweils mit einer Übersetzungseinheit. Eine Klasse aus einer anderen Übersetzungseinheit, z. B. class D : public B, könnte die Methode überschreiben. Der Compiler kann jedoch zu dem Zeitpunkt, an dem der Aufruf an b->print() übersetzt wird, keine Sichtbarkeit in die Übersetzungseinheit class D haben, so dass der Compiler einen virtuellen Aufruf annehmen muss.

Um diesen Mangel zu beheben, hat C++ 11 final keyword eingeführt, mit dem Programmierer dem Compiler mitteilen können, dass von dieser Ebene der Vererbungshierarchie keine weiteren Außerkraftsetzungen folgen würden. Jetzt kann der Compiler den virtuellen Aufruf optimieren und auch die Forderung nach weiteren Überschreibungen erzwingen.

+1

Was ist, wenn "final" hinzugefügt wird? – vsoftco

+1

Mit LTO kann der Linker sehen, dass es ungenutzt ist und es devirtualisieren, würde ich annehmen. –

+2

@vsoftco Guter Punkt - Ich erwähnte 'final' in der Antwort. – dasblinkenlight

2

In Bezug auf

Theoretisch kann es bei der Kompilierung-Zeit bestimmt werden, weil wir wissen, dass keiner der Unterklassen von B diese Methode überschreibt, also egal, was ich in den Zeiger auf BB zuweisen * b = neues C; b-> print() ;, wird immer B :: print() aufgerufen.

Ja.

Ob der Compiler diese Optimierung durchführen wird, hängt jedoch vollständig vom Compiler ab, was er weiß und was Sie ihm sagen.

Was der Compiler weiß, hängt von vielen Faktoren ab, wie zum Beispiel

  • Gibt es definierte Klassen in mehreren Übersetzungseinheiten, die separat kompiliert werden?

  • Verwenden Sie globale Optimierung?

  • Verwenden Sie vielleicht das Schlüsselwort final, um den Compiler zu informieren?

Mit Ihrem speziellen Beispiel

B * b = new B; 
b->print(); 

wo print virtuell, ich ziemlich sicher fühlen würde, dass es nicht so gut wie unabhängig von Compiler aufgerufen werden würde, weil hier der Compiler weiß, welche b bezieht sich auf. Lass uns das Prüfen.

OK, mit MinGW g ++ 5.1 und Option -O2 (Ich versuche nicht irgendetwas anderes) wird der Anruf zu einem direkten Aufruf von puts zusammengestellt nach unten, auch die printf umgehen.

+0

Um fair zu sein, ruft Ihr Compiler 'puts' direkt auf, weil er festgestellt hat, dass' b' vom Typ 'B' ist, nicht weil er festgestellt hat, dass keine Überschreibungen von' print' existieren. – dasblinkenlight

+0

Wie überprüfen Sie, welcher Anruftyp angewendet wurde? – Youda008

+0

@ Youda008: Hängt davon ab. In diesem Fall habe ich die Kommandozeile 'g ++ -O2 -S -Masm = intel foo.cpp' benutzt und dann die Assembly in' foo.s' inspiziert. –

1

Standard nebenbei, moderne Compiler haben auch Kompilieren Sachen wie DLLs und gemeinsame Bibliotheken, und beschäftigen sich mit einem Fall von rohen Shared Memory, die anderen Prozess new ed einige Objektinstanz in.

so ist es nicht ungewöhnlich, einen "Common" oder "Shared" Ordner zu sehen, der eine Schnittstelle einer Klasse enthält, und zwei Projekte enthalten diesen Header und werden von dieser Schnittstelle abgeleitet.

so in Ihrem Beispiel, sagen wir mal, die ganze Erklärungen in einer Header-Datei sind, können einige andere Projekt, das Header enthalten und leiten sich von B

dies ist, wie Sie eine Klasse Zeiger aus einer DLL exportieren und rufen Sie die rechte Funktion von gemeinsam genutzten Bibliotheken.

und wie ich schrieb in den Kommentar: virtuelle Funktionen hat einen Overhead, aber 2 ~ 3 weitere Montagelinien sowieso. Sie laden die V-Tabelle in ein Register, inkrementieren dieses Register um einen Offset und Sie sind fertig. Sie müssen vituale Funktionen Milliarden von Zeiten aufrufen, um eine Sekunde einer Differenz zu sehen. Übliche Engpässe sind nicht-freundliche Cache-Variablen, Speicherzuweisungen, schwere CPU und IO sowieso, nicht virtuelle Funktionen

Verwandte Themen