5 Stimmen

Linkerfehler beim rein virtuellen Funktionsaufruf mit gcc

Ein Freund und ich hatten eine sehr interessante Diskussion über die Konstruktion von Objekten, die mit diesem Code endete:

#include 

class Parent {
  public:
    Parent( ) {
      this->doSomething( );
    }

    virtual void doSomething( ) = 0;
};

class Child : public Parent {
    int param;

  public:
    Child( ) {
      param = 1000;
    }

    virtual void doSomething( ) {
      std::cout << "doSomething( " << param << " )" << std::endl;
    }
};

int main( void ) {
    Child c;
    return 0;
}

Ich weiß, dass der Standard das Verhalten nicht definiert, wenn eine reine virtuelle Funktion aus einem Konstruktor oder Destruktor aufgerufen wird, außerdem ist dies kein praktisches Beispiel dafür, wie ich Code in der Produktion schreiben würde, es handelt sich nur um einen Test, um zu überprüfen, was der Compiler tut.

Bei der Prüfung desselben Konstrukts in Java wird Folgendes ausgegeben

doSomething( 0 )

Das ergibt Sinn, da param zum Zeitpunkt des Aufrufs von doSomething() aus dem Elternkonstruktor nicht initialisiert ist.

Ich würde ein ähnliches Verhalten in C++ erwarten, mit dem Unterschied, dass param irgendetwas zum Zeitpunkt des Funktionsaufrufs enthält.

Stattdessen führt das Kompilieren des obigen Codes zu einem Linkerfehler mit (c++ (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3), der besagt, dass die Referenz auf Parent::doSomething( ) undefiniert ist.

Also lautet meine Frage: Warum ist dies ein Linkerfehler? Wenn dies ein Fehler ist, würde ich erwarten, dass der Compiler sich beschwert, insbesondere weil eine Implementierung der Funktion vorhanden ist. Jede Information darüber, wie der Linker in diesem Fall arbeitet, oder eine Verweis auf weiterführende Literatur wäre sehr willkommen.

Vielen Dank im Voraus! Ich hoffe, dass diese Frage nicht doppelt gestellt wurde, aber ich konnte keine ähnliche Frage finden.

5voto

Hampus Nilsson Punkte 6542

Der Compiler weiß, dass, wenn Sie doSomething innerhalb des Konstruktors aufrufen, dieser Aufruf auf doSomething in dieser Klasse verweisen muss, auch wenn es virtuell ist. Daher optimiert er den virtuellen Dispatch weg und führt stattdessen einfach einen normalen Funktionsaufruf durch. Da die Funktion nicht definiert ist, führt dies zu einem Fehler zur Link-Zeit.

5voto

Also, meine Frage ist: Warum ist das ein Linkerfehler? Wenn dies ein Fehler ist, würde ich erwarten, dass der Compiler sich beschwert, insbesondere weil es eine Implementierung der Funktion gibt. Jeder Einblick darüber, wie der Linker in diesem Fall funktioniert oder ein Verweis auf weiterführende Literatur wäre sehr willkommen.

Lassen Sie uns das ein wenig erweitern.

Warum ist es ein Linkerfehler?

Weil der Compiler einen Aufruf von Parent::doSomething() vom Konstruktor aus eingefügt hat, der Linker jedoch keine Definition der Funktion gefunden hat.

Ich würde erwarten, dass der Compiler sich beschwert, insbesondere weil es eine Implementierung der Funktion gibt.

Das ist nicht korrekt. Es gibt ein Override für diese Funktion, auf die über die virtuelle Dispatch zugegriffen werden kann, aber die Funktion Parent::doSomething() ist nicht definiert. Es gibt einen subtilen, aber wichtigen Unterschied, der auf eine andere Weise getestet werden kann, indem die dynamische Dispatch deaktiviert wird. Sie können die dynamische Dispatch für einen bestimmten Aufruf deaktivieren, indem Sie die Funktion mit dem Klassennamen qualifizieren, z. B. in Child::doSomething(), wenn Sie Parent::doSomething() hinzufügen, wird dies einen Aufruf von Parent::doSomething() generieren, ohne die dynamische Dispatch zu verwenden, um den endgültigen Override aufzurufen.

Warum ist das wichtig?

Das ist wichtig, weil selbst wenn die Funktion rein-virtuell ist (rein virtuell bedeutet, dass die dynamische Dispatch niemals zu dieser bestimmten Überladung führen wird), sie dennoch definiert und aufgerufen werden kann:

struct base {
   virtual void f() = 0;
};
inline void base::f() { std::cout << "base\n"; }
struct derived : base {
   virtual void f() {
      base::f();
      std::cout << "derived\n";
   }
};
int main() {
   derived d;
   d.f();        // gibt aus: base derived
}

Jetzt hat C++ ein separates Kompiliermodell, das bedeutet, dass die Funktionen nicht in dieser speziellen Übersetzungseinheit definiert sein müssen. Das heißt, Parent::doSomething() kann in einer anderen Übersetzungseinheit definiert sein, die in dasselbe Programm eingebunden wird. Der Compiler kann unmöglich wissen, ob eine andere Translationseinheit diese Funktion definieren wird, nur der Linker weiß das, und deshalb ist es der Linker, der sich beschwert.

Jeder Einblick darüber, wie der Linker in diesem Fall funktioniert oder ein Verweis auf weiterführende Literatur wäre sehr willkommen.

Wie bereits erwähnt, fügt der Compiler (dieser bestimmte Compiler) einen Aufruf des bestimmten Overrides auf der Parent-Ebene hinzu. Der Linker versucht wie bei allen anderen Funktionsaufrufen, dieses Symbol in einer der Übersetzungseinheiten zu finden und scheitert dabei, was den Fehler auslöst.

Der pure-virtual-Spezifizierer dient allein dazu, die implizite Verwendung (odr-use im Standard) der Funktion beim Erstellen der virtuellen Tabelle zu vermeiden. Das heißt, der einzige Zweck des reinen Spezifizierers besteht darin, keine Abhängigkeit von diesem Symbol in der virtuellen Tabelle hinzuzufügen. Das bedeutet wiederum, dass der Linker das Vorhandensein des Symbols (Parent::doSomething) im Programm für den Zweck der dynamischen Dispatch (vtable) nicht benötigen wird, aber er wird das Symbol immer noch benötigen, wenn es explizit vom Programm verwendet wird.

0voto

Torsten Robitzki Punkte 3019

Beim Aufruf von doSomething() kann der Compiler sicher sein, dass der dynamische Typ von *this Parent ist, daher ist es nicht erforderlich, die Funktion indirekt aufzurufen. Dieses Verhalten hängt stark vom Compiler/Linker ab, den Sie verwenden.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X