Wann kann ich eine Forward-Erklärung verwenden?

602

Ich suche nach der Definition, wann ich eine Vorwärtsdeklaration einer Klasse in der Header-Datei einer anderen Klasse durchführen darf:

Darf ich dies für eine Basisklasse, für eine als Mitglied gehaltene Klasse, für eine Klasse tun, die als Referenz an die Mitgliedsfunktion übergeben wurde usw.?

Igor Oks
quelle
14
Ich möchte unbedingt, dass dies in "Wann sollte ich" umbenannt und die Antworten entsprechend aktualisiert werden ...
Deworde
12
@deworde Wenn Sie sagen, wann "sollte", fragen Sie nach einer Meinung.
AturSams
@deworde Ich verstehe, dass Sie Forward-Deklarationen verwenden möchten, wann immer Sie können, um die Erstellungszeit zu verbessern und Zirkelverweise zu vermeiden. Die einzige Ausnahme, an die ich denken kann, ist, wenn eine Include-Datei typedefs enthält. In diesem Fall besteht ein Kompromiss zwischen der Neudefinition des typedef (und dem Risiko, dass es sich ändert) und dem Einschließen einer gesamten Datei (zusammen mit ihren rekursiven Includes).
Ohad Schneider
@OhadSchneider Aus praktischer Sicht bin ich kein großer Fan von Headern, die meine. ÷
Deworde
Grundsätzlich müssen Sie immer einen anderen Header
einfügen

Antworten:

962

Versetzen Sie sich in die Position des Compilers: Wenn Sie einen Typ weiterleiten, weiß der Compiler nur, dass dieser Typ existiert. es weiß nichts über seine Größe, Mitglieder oder Methoden. Aus diesem Grund wird es als unvollständiger Typ bezeichnet . Daher können Sie den Typ nicht zum Deklarieren eines Mitglieds oder einer Basisklasse verwenden, da der Compiler das Layout des Typs kennen müsste.

Angenommen, die folgende Vorwärtserklärung.

class X;

Folgendes können und können Sie nicht tun.

Was Sie mit einem unvollständigen Typ tun können:

  • Deklarieren Sie ein Mitglied als Zeiger oder Verweis auf den unvollständigen Typ:

    class Foo {
        X *p;
        X &r;
    };
  • Deklarieren Sie Funktionen oder Methoden, die unvollständige Typen akzeptieren / zurückgeben:

    void f1(X);
    X    f2();
  • Definieren Sie Funktionen oder Methoden, die Zeiger / Verweise auf den unvollständigen Typ akzeptieren / zurückgeben (jedoch ohne Verwendung seiner Elemente):

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}

Was Sie mit einem unvollständigen Typ nicht machen können:

  • Verwenden Sie es als Basisklasse

    class Foo : X {} // compiler error!
  • Verwenden Sie es, um ein Mitglied zu deklarieren:

    class Foo {
        X m; // compiler error!
    };
  • Definieren Sie Funktionen oder Methoden mit diesem Typ

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • Verwenden Sie seine Methoden oder Felder und versuchen Sie tatsächlich, eine Variable mit unvollständigem Typ zu dereferenzieren

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };

Bei Vorlagen gibt es keine absolute Regel: Ob Sie einen unvollständigen Typ als Vorlagenparameter verwenden können, hängt davon ab, wie der Typ in der Vorlage verwendet wird.

Zum Beispiel std::vector<T>muss der Parameter ein vollständiger Typ sein, während boost::container::vector<T>dies nicht der Fall ist. Manchmal ist ein vollständiger Typ nur erforderlich, wenn Sie bestimmte Elementfunktionen verwenden. Dies ist beispielsweise der Fallstd::unique_ptr<T> .

Eine gut dokumentierte Vorlage sollte in ihrer Dokumentation alle Anforderungen ihrer Parameter angeben, einschließlich der Frage, ob es sich um vollständige Typen handeln muss oder nicht.

Luc Touraille
quelle
4
Tolle Antwort, aber bitte sehen Sie meine unten für den technischen Punkt, in dem ich nicht einverstanden bin. Kurz gesagt, wenn Sie keine Header für unvollständige Typen einschließen, die Sie akzeptieren oder zurückgeben, erzwingen Sie eine unsichtbare Abhängigkeit davon, dass der Verbraucher Ihres Headers wissen muss, welche anderen er benötigt.
Andy Dent
2
@AndyDent: Richtig, aber der Konsument des Headers muss nur die Abhängigkeiten angeben, die er tatsächlich verwendet. Dies folgt dem C ++ - Prinzip "Sie zahlen nur für das, was Sie verwenden". In der Tat kann es für den Benutzer unpraktisch sein, der erwarten würde, dass der Header eigenständig ist.
Luc Touraille
8
Dieses Regelwerk ignoriert einen sehr wichtigen Fall: Sie benötigen einen vollständigen Typ, um die meisten Vorlagen in der Standardbibliothek zu instanziieren. Dies muss besonders beachtet werden, da ein Verstoß gegen die Regel zu undefiniertem Verhalten führt und möglicherweise keinen Compilerfehler verursacht.
James Kanze
12
+1 für "Versetzen Sie sich in die Position des Compilers". Ich stelle mir vor, dass der "Compiler" einen Schnurrbart hat.
PascalVKooten
3
@JesusChrist: Genau: Wenn Sie ein Objekt als Wert übergeben, muss der Compiler seine Größe kennen, um die entsprechende Stapelmanipulation durchführen zu können. Beim Übergeben eines Zeigers oder einer Referenz benötigt der Compiler nicht die Größe oder das Layout des Objekts, sondern nur die Größe einer Adresse (dh die Größe eines Zeigers), die nicht vom Typ abhängt, auf den verwiesen wird.
Luc Touraille
45

Die Hauptregel lautet, dass Sie nur Klassen vorwärts deklarieren können, deren Speicherlayout (und damit Elementfunktionen und Datenelemente) in der Datei, die Sie weiterleiten, nicht bekannt sein müssen.

Dies würde Basisklassen und alles andere als Klassen ausschließen, die über Referenzen und Zeiger verwendet werden.

Timo Geusch
quelle
6
Fast. Sie können auch auf "einfache" (dh Nicht-Zeiger / Referenz) unvollständige Typen als Parameter oder Rückgabetypen in Funktionsprototypen verweisen.
j_random_hacker
Was ist mit Klassen, die ich als Mitglieder einer Klasse verwenden möchte, die ich in der Header-Datei definiere? Kann ich sie weiterleiten?
Igor Oks
1
Ja, aber in diesem Fall können Sie nur eine Referenz oder einen Zeiger auf die vorwärts deklarierte Klasse verwenden. Aber Sie können trotzdem Mitglieder haben.
Reunanen
32

Lakos unterscheidet zwischen Klassengebrauch

  1. Nur im Namen (für die eine Vorwärtserklärung ausreicht) und
  2. in-size (für die die Klassendefinition benötigt wird).

Ich habe es noch nie so prägnant ausgesprochen gesehen :)

Marc Mutz - mmutz
quelle
2
Was bedeutet "Nur im Namen"?
Boon
4
@Boon: wage ich es zu sagen ...? Wenn Sie nur den Klassennamen verwenden ?
Marc Mutz - mmutz
1
Plus eins für Lakos, Marc
mlvljr
28

Neben Zeigern und Verweisen auf unvollständige Typen können Sie auch Funktionsprototypen deklarieren, die Parameter angeben und / oder Werte zurückgeben, die unvollständige Typen sind. Sie können jedoch keine Funktion mit einem unvollständigen Parameter oder Rückgabetyp definieren , es sei denn, es handelt sich um einen Zeiger oder eine Referenz.

Beispiele:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types
j_random_hacker
quelle
19

Keine der bisherigen Antworten beschreibt, wann eine Vorwärtsdeklaration einer Klassenvorlage verwendet werden kann. Also, hier geht es.

Eine Klassenvorlage kann deklariert weitergeleitet werden als:

template <typename> struct X;

Im Anschluss an der Struktur der akzeptierten Antwort ,

Folgendes können und können Sie nicht tun.

Was Sie mit einem unvollständigen Typ tun können:

  • Deklarieren Sie ein Mitglied als Zeiger oder Verweis auf den unvollständigen Typ in einer anderen Klassenvorlage:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
  • Deklarieren Sie ein Mitglied als Zeiger oder Verweis auf eine seiner unvollständigen Instanziierungen:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • Deklarieren Sie Funktionsvorlagen oder Elementfunktionsvorlagen, die unvollständige Typen akzeptieren / zurückgeben:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • Deklarieren Sie Funktionen oder Elementfunktionen, die eine ihrer unvollständigen Instanziierungen akzeptieren / zurückgeben:

    void      f1(X<int>);
    X<int>    f2();
  • Definieren Sie Funktionsvorlagen oder Elementfunktionsvorlagen, die Zeiger / Verweise auf den unvollständigen Typ akzeptieren / zurückgeben (jedoch ohne Verwendung seiner Elemente):

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
  • Definieren Sie Funktionen oder Methoden, die Zeiger / Verweise auf eine ihrer unvollständigen Instanziierungen akzeptieren / zurückgeben (jedoch ohne Verwendung ihrer Mitglieder):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • Verwenden Sie es als Basisklasse einer anderen Vorlagenklasse

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Verwenden Sie diese Option, um ein Mitglied einer anderen Klassenvorlage zu deklarieren:

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Definieren Sie Funktionsvorlagen oder Methoden mit diesem Typ

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }

Was Sie mit einem unvollständigen Typ nicht machen können:

  • Verwenden Sie eine seiner Instanziierungen als Basisklasse

    class Foo : X<int> {} // compiler error!
  • Verwenden Sie eine seiner Instanziierungen, um ein Mitglied zu deklarieren:

    class Foo {
        X<int> m; // compiler error!
    };
  • Definieren Sie Funktionen oder Methoden mit einer ihrer Instanziierungen

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Verwenden Sie die Methoden oder Felder einer ihrer Instanziierungen und versuchen Sie tatsächlich, eine Variable mit unvollständigem Typ zu dereferenzieren

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • Erstellen Sie explizite Instanziierungen der Klassenvorlage

    template struct X<int>;
R Sahu
quelle
2
"Keine der Antworten beschreibt bisher, wann man die Vorwärtsdeklaration einer Klassenvorlage kann." Ist das nicht einfach so, weil die Semantik von Xund X<int>genau gleich ist und nur die vorwärts deklarierende Syntax sich in irgendeiner wesentlichen Weise unterscheidet, wobei alle bis auf eine Zeile Ihrer Antwort nur Lucs und bedeuten s/X/X<int>/g? Wird das wirklich gebraucht? Oder habe ich ein kleines Detail übersehen, das anders ist? Es ist möglich, aber ich habe ein paar Mal visuell verglichen und kann keine sehen ...
underscore_d
Vielen Dank! Diese Bearbeitung fügt eine Tonne wertvoller Informationen hinzu. Ich muss es mehrmals lesen, um es vollständig zu verstehen ... oder vielleicht die oft bessere Taktik anwenden, zu warten, bis ich in echtem Code schrecklich verwirrt bin und hierher zurückkomme! Ich vermute, dass ich damit Abhängigkeiten an verschiedenen Stellen reduzieren kann.
underscore_d
4

In einer Datei, in der Sie nur Zeiger oder Verweis auf eine Klasse verwenden. Und es sollte keine Member / Member-Funktion aufgerufen werden, wenn diese Zeiger / Verweise vorhanden sind.

mit class Foo;// Vorwärtsdeklaration

Wir können Datenelemente vom Typ Foo * oder Foo & deklarieren.

Wir können Funktionen mit Argumenten und / oder Rückgabewerten vom Typ Foo deklarieren (aber nicht definieren).

Wir können statische Datenelemente vom Typ Foo deklarieren. Dies liegt daran, dass statische Datenelemente außerhalb der Klassendefinition definiert sind.

yesraaj
quelle
4

Ich schreibe dies als separate Antwort und nicht nur als Kommentar, da ich der Antwort von Luc Touraille nicht zustimme, nicht aus Gründen der Legalität, sondern wegen robuster Software und der Gefahr von Fehlinterpretationen.

Insbesondere habe ich ein Problem mit dem impliziten Vertrag darüber, was Benutzer Ihrer Benutzeroberfläche wissen müssen.

Wenn Sie Referenztypen zurückgeben oder akzeptieren, sagen Sie nur, dass sie einen Zeiger oder eine Referenz passieren können, die sie möglicherweise nur durch eine Vorwärtsdeklaration kennen.

Wenn Sie einen unvollständigen Typen kehren X f2();dann Anrufer sagen Sie müssen die vollständige Typangabe von X haben Sie brauchen es , um das LHS oder temporäres Objekt an der Aufrufstelle zu erstellen.

Wenn Sie einen unvollständigen Typ akzeptieren, muss der Aufrufer das Objekt erstellt haben, das der Parameter ist. Selbst wenn dieses Objekt als ein anderer unvollständiger Typ von einer Funktion zurückgegeben wurde, benötigt die Aufrufsite die vollständige Deklaration. dh:

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

Ich denke, es gibt ein wichtiges Prinzip, dass ein Header genügend Informationen liefern sollte, um sie zu verwenden, ohne dass eine Abhängigkeit andere Header erfordert. Das bedeutet, dass der Header in eine Kompilierungseinheit aufgenommen werden kann, ohne einen Compilerfehler zu verursachen, wenn Sie deklarierte Funktionen verwenden.

Außer

  1. Wenn diese externe Abhängigkeit gewünschtes Verhalten ist. Statt bedingte Kompilierung verwenden könnten Sie eine haben gut dokumentierte Anforderung für sie , ihre eigenen Header erklärt X. Diese liefern eine Alternative zu #ifdefs mit und kann eine nützliche Art und Weise sein Mocks oder andere Varianten vorstellen.

  2. Der wichtige Unterschied sind einige Vorlagentechniken, bei denen ausdrücklich nicht erwartet wird, dass Sie sie instanziieren. Sie werden nur erwähnt, damit jemand nicht mit mir bissig wird.

Andy Dent
quelle
"Ich denke, es gibt ein wichtiges Prinzip, dass ein Header genügend Informationen liefern sollte, um sie zu verwenden, ohne dass eine Abhängigkeit andere Header erfordert." - Ein weiteres Problem wird in einem Kommentar von Adrian McCarthy zu Naveens Antwort erwähnt. Dies ist ein guter Grund, sich nicht an das Prinzip "sollte genügend Informationen liefern, um es zu verwenden" zu halten, selbst für Typen, die derzeit keine Vorlagen haben.
Tony Delroy
3
Sie sprechen darüber, wann Sie die Vorwärtsdeklaration verwenden sollten (oder nicht). Das ist jedoch absolut nicht der Punkt dieser Frage. Hier geht es darum, die technischen Möglichkeiten zu kennen, wenn (zum Beispiel) ein zirkuläres Abhängigkeitsproblem gelöst werden soll.
JonnyJD
1
I disagree with Luc Touraille's answerSchreiben Sie ihm also einen Kommentar, einschließlich eines Links zu einem Blog-Beitrag, wenn Sie die Länge benötigen. Dies beantwortet die gestellte Frage nicht. Wenn sich alle Gedanken darüber machen würden, wie X funktioniert, begründete Antworten, die nicht mit X übereinstimmen, oder wenn wir über Grenzen debattieren, innerhalb derer wir unsere Freiheit, X zu verwenden, einschränken sollten, hätten wir fast keine wirklichen Antworten.
underscore_d
3

Die allgemeine Regel, der ich folge, ist, keine Header-Datei einzuschließen, es sei denn, ich muss. Wenn ich also das Objekt einer Klasse nicht als Mitgliedsvariable meiner Klasse speichere, werde ich es nicht einschließen, sondern nur die Vorwärtsdeklaration verwenden.

Naveen
quelle
2
Dies unterbricht die Kapselung und macht den Code spröde. Dazu müssen Sie wissen, ob der Typ ein typedef oder eine Klasse für eine Klassenvorlage mit Standardvorlagenparametern ist. Wenn sich die Implementierung jemals ändert, müssen Sie jeden Ort aktualisieren, an dem Sie eine Weiterleitungsdeklaration verwendet haben.
Adrian McCarthy
@AdrianMcCarthy ist richtig, und eine vernünftige Lösung besteht darin, einen Forward-Deklarations-Header zu haben, der in dem Header enthalten ist, dessen Inhalt er weiterleitet. Dieser sollte Eigentum desjenigen sein / gepflegt / versendet werden, der auch diesen Header besitzt. Beispiel: Der Header der iosfwd Standard-Bibliothek, der Vorwärtsdeklarationen von iostream-Inhalten enthält.
Tony Delroy
3

Solange Sie die Definition nicht benötigen (denken Sie an Zeiger und Referenzen), können Sie mit Vorwärtsdeklarationen davonkommen. Aus diesem Grund werden sie meistens in Headern angezeigt, während Implementierungsdateien normalerweise den Header für die entsprechende (n) Definition (en) abrufen.

dirkgently
quelle
0

Normalerweise möchten Sie die Vorwärtsdeklaration in einer Klassenheaderdatei verwenden, wenn Sie den anderen Typ (Klasse) als Mitglied der Klasse verwenden möchten. Sie können nicht , die zukunfts angegebenen Klassen verwenden Methoden in der Header - Datei , weil C ++ nicht die Definition dieser Klasse zu diesem Zeitpunkt noch nicht kennt. Das ist Logik, die Sie in die CPP-Dateien verschieben müssen, aber wenn Sie Vorlagenfunktionen verwenden, sollten Sie diese auf den Teil reduzieren, der die Vorlage verwendet, und diese Funktion in den Header verschieben.

Patrick Glandien
quelle
Das macht keinen Sinn. Man kann kein Mitglied eines unvollständigen Typs haben. Die Deklaration einer Klasse muss alles enthalten, was alle Benutzer über ihre Größe und ihr Layout wissen müssen. Seine Größe umfasst die Größe aller nicht statischen Elemente. Wenn ein Mitglied vorwärts deklariert wird, haben Benutzer keine Ahnung von seiner Größe.
underscore_d
0

Nehmen wir an, dass die Vorwärtsdeklaration Ihren Code zum Kompilieren bringt (obj wird erstellt). Die Verknüpfung (exe-Erstellung) ist jedoch nur dann erfolgreich, wenn die Definitionen gefunden wurden.

Sesh
quelle
2
Warum haben 2 Leute dies jemals positiv bewertet? Sie sprechen nicht darüber, worüber die Frage spricht. Sie meinen normale - nicht vorwärts - Deklaration von Funktionen . Die Frage betrifft die Vorwärtsdeklaration von Klassen . Wie Sie sagten "Vorwärtsdeklaration bringt Ihren Code zum Kompilieren", tun Sie mir einen Gefallen: Kompilieren Sie class A; class B { A a; }; int main(){}und lassen Sie mich wissen, wie das geht. Natürlich wird es nicht kompiliert. Alle richtigen Antworten hier erklären , warum und die genauen, beschränkt Kontexte , in denen zukunfts Erklärung ist gültig. Sie haben dies stattdessen über etwas völlig anderes geschrieben.
underscore_d
0

Ich möchte nur eine wichtige Sache hinzufügen, die Sie mit einer weitergeleiteten Klasse tun können, die in der Antwort von Luc Touraille nicht erwähnt wird.

Was Sie mit einem unvollständigen Typ tun können:

Definieren Sie Funktionen oder Methoden, die Zeiger / Verweise auf den unvollständigen Typ akzeptieren / zurückgeben, und leiten Sie diese Zeiger / Verweise auf eine andere Funktion weiter.

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

Ein Modul kann ein Objekt einer vorwärts deklarierten Klasse an ein anderes Modul übergeben.

Netter Mann
quelle
"eine weitergeleitete Klasse" und "eine weitergeleitete deklarierte Klasse" könnten sich fälschlicherweise auf zwei sehr unterschiedliche Dinge beziehen. Was Sie geschrieben haben, folgt direkt aus Konzepten, die in Lucs Antwort enthalten sind. Obwohl es einen guten Kommentar gegeben hätte, der eine klare Klarstellung hinzufügt, bin ich mir nicht sicher, ob es eine Antwort rechtfertigt.
underscore_d
0

Wie Luc Touraille bereits sehr gut erklärt hat, wo die Vorwärtsdeklaration der Klasse verwendet werden soll und nicht.

Ich werde nur hinzufügen, warum wir es verwenden müssen.

Wir sollten nach Möglichkeit die Vorwärtsdeklaration verwenden, um die unerwünschte Abhängigkeitsinjektion zu vermeiden.

Da #includeHeader-Dateien zu mehreren Dateien hinzugefügt werden, wird beim Hinzufügen eines Headers zu einer anderen Header-Datei eine unerwünschte Abhängigkeitsinjektion in verschiedenen Teilen des Quellcodes hinzugefügt. #includeDies kann vermieden werden, indem Header in .cppDateien hinzugefügt werden, wo immer dies möglich ist, anstatt zu einer anderen Header-Datei und hinzuzufügen Verwenden Sie nach Möglichkeit die Klassenvorwärtsdeklaration in Header- .hDateien.

A 786
quelle