Sollte man Forward-Deklarationen anstelle von Includes verwenden, wo immer es möglich ist?

Wenn eine classndeklaration eine andere class nur als pointers verwendet, macht es Sinn, eine class-Forward-Deklaration anstelle der Headerdatei zu verwenden, um Probleme mit zirkulären Abhängigkeiten präventiv zu vermeiden? Also, anstatt:

//file Ch #include "Ah" #include "Bh" class C{ A* a; B b; ... }; 

Tu dies stattdessen:

 //file Ch #include "Bh" class A; class C{ A* a; B b; ... }; //file C.cpp #include "Ch" #include "Ah" ... 

Gibt es irgendeinen Grund, warum das nicht zu tun, wo immer es möglich ist?

   

    Die Methode der Vorwärtsdeklaration ist fast immer besser. (Ich kann mir keine Situation vorstellen, in der eine Datei enthalten ist, in der Sie eine Vorwärtsdeklaration verwenden können, aber ich werde nicht sagen, dass es immer besser ist, nur für den Fall).

    Es gibt keine Nachteile bei der Vorwärtsdeklaration von classn, aber ich kann mir ein paar Nachteile vorstellen, wenn man Header unnötigerweise einfügt:

    • längere Übersetzungszeit, da alle Übersetzungseinheiten einschließlich Ch auch Ah , obwohl sie dies möglicherweise nicht benötigen.

    • möglicherweise einschließlich anderer Header, die Sie nicht indirekt benötigen

    • Verschmutzung der Übersetzungseinheit mit Symbolen, die Sie nicht benötigen

    • Sie müssen möglicherweise Quelldateien neu kompilieren, die diesen Header enthalten, wenn sich der Header ändert (@PeterWood)

    Ja, Vorwärtsdeklarationen sind immer besser.

    Einige der Vorteile, die sie bieten, sind:

    • Reduzierte Kompilierungszeit
    • Kein Namespace verschmutzt.
    • (In einigen Fällen) kann die Größe der generierten Binärdateien reduzieren.
    • Die Rekompilierungszeit kann erheblich verkürzt werden.
    • Vermeiden eines möglichen Zusammenstoßes von Präprozessornamen.
    • Die Implementierung von PIMPL Idiom bietet somit die Möglichkeit, die Implementierung von der Schnittstelle zu verbergen.

    Forward, das eine class deklariert, macht jedoch diese class zu einem unvollständigen Typ, und das schränkt die Operationen, die Sie für den Typ Unvollständig ausführen können, stark ein.
    Sie können keine Operationen ausführen, bei denen der Compiler das Layout der class kennen müsste.

    Mit Unvollständig können Sie:

    • Deklarieren Sie ein Member als einen pointers oder einen Verweis auf den unvollständigen Typ.
    • Deklariere functionen oder Methoden, die unvollständige Typen akzeptieren / zurückgeben.
    • Definieren Sie functionen oder Methoden, die pointers / Referenzen auf den unvollständigen Typ akzeptieren / zurückgeben (aber ohne seine Mitglieder zu verwenden).

    Bei unvollständiger Art können Sie nicht:

    • Verwenden Sie es als Basisklasse.
    • Verwenden Sie es, um ein Mitglied zu deklarieren.
    • Definieren Sie functionen oder Methoden, die diesen Typ verwenden.

    Gibt es irgendeinen Grund, warum das nicht zu tun, wo immer es möglich ist?

    Bequemlichkeit.

    Wenn Sie vor der Phase wissen, dass jeder Benutzer dieser Header-Datei notwendigerweise auch die Definition von A , um irgendetwas zu tun (oder vielleicht die meiste Zeit). Dann ist es bequem, es einfach ein für allemal aufzunehmen.

    Dies ist ein ziemlich heikles Thema, da ein zu liberaler Gebrauch dieser Daumenregel einen nahezu unkompilierbaren Code ergibt. Beachten Sie, dass Boost das Problem anders anwendet, indem spezifische “Convenience” -Kopfzeilen bereitgestellt werden, die eine Reihe enger functionalitäten zusammenfassen.

    Ein Fall, in dem Sie keine Vorwärtsdeklarationen haben möchten, ist, wenn sie selbst schwierig sind. Dies kann vorkommen, wenn einige Ihrer classn wie im folgenden Beispiel eine Vorlage haben:

     // Forward declarations template  class Frobnicator; template  > class Gibberer; // Alternative: more clear to the reader; more stable code #include "Gibberer.h" // Declare a function that does something with a pointer int do_stuff(Gibberer*); 

    Vorwärts-Deklarationen sind die gleichen wie bei der Code-Duplizierung: Wenn der Code dazu neigt, sich stark zu ändern, müssen Sie ihn jedes Mal an 2 oder mehr Stellen ändern, und das ist nicht gut.

    Sollte man Forward-Deklarationen anstelle von Includes verwenden, wo immer es möglich ist?

    Nein, explizite Weiterleitungserklärungen sollten nicht als allgemeine Richtlinie betrachtet werden. Vorwärtsdeklarationen sind im Wesentlichen kopierte und eingefügte oder falsch geschriebene Codes, die, falls Sie einen Fehler darin finden, überall dort behoben werden müssen, wo die Vorwärtsdeklarationen verwendet werden. Dies kann errorsanfällig sein.

    Um Fehlanpassungen zwischen den “forward” -Deklarationen und ihren Definitionen zu vermeiden, fügen Sie Deklarationen in eine Header-Datei ein und fügen Sie diese Header-Datei sowohl in die definierenden als auch in die deklarationsverwendenden Quelldateien ein.

    In diesem speziellen Fall, in dem nur eine undurchsichtige class vorwärts deklariert wird, kann diese Forward-Deklaration in Ordnung sein, aber im Allgemeinen “Forward-Deklarationen anstelle von Includes verwenden, wann immer es möglich ist”, wie der Titel dieses Threads sagt ziemlich riskant sein.

    Hier sind einige Beispiele für “unsichtbare Risiken” in Bezug auf Forward-Deklarationen (unsichtbare Risiken = Deklarationsinkongruenzen, die vom Compiler oder Linker nicht erkannt werden):

    • Explizite Vorwärtsdeklarationen von Symbolen, die Daten darstellen, können unsicher sein, da solche Vorwärtsdeklarationen möglicherweise eine korrekte Kenntnis des Footprints (Größe) des Datentyps erfordern.

    • Explizite Vorwärtsdeklarationen von Symbolen, die functionen darstellen, können ebenfalls unsicher sein, wie die Parametertypen und die Anzahl von Parametern.

    Das folgende Beispiel verdeutlicht dies, zB zwei gefährliche Vorwärtsdeklarationen von Daten sowie einer function:

    Datei ac:

     #include  char data[128][1024]; extern "C" void function(short truncated, const char* forgotten) { std::cout < < "truncated=" << std::hex << truncated << ", forgotten=\"" << forgotten << "\"\n"; } 

    Datei bc:

     #include  extern char data[1280][1024]; // 1st dimension one decade too large extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param int main() { function(0x1234abcd); // In worst case: - No crash! std::cout < < "accessing data[1270][1023]\n"; return (int) data[1270][1023]; // In best case: - Boom !!!! } 

    Kompilieren des Programms mit g ++ 4.7.1:

     > g++ -Wall -pedantic -ansi ac bc 

    Hinweis: Unsichtbare Gefahr, da g ++ keine Compiler- oder Linker-Fehler / Warnungen liefert
    Hinweis: Das Entfernen von extern "C" führt zu einem Verknüpfungserrors für function() aufgrund der C ++ - Namensänderung.

    Ausführen des Programms:

     > ./a.out truncated=abcd, forgotten="♀♥♂☺☻" accessing data[1270][1023] Segmentation fault 

    Interessanterweise empfiehlt Google in seinem C ++ – Styleguide , überall #include zu verwenden, um zirkuläre Abhängigkeiten zu vermeiden.

    Gibt es irgendeinen Grund, warum das nicht zu tun, wo immer es möglich ist?

    Absolut: Es bricht die Kapselung ab, indem der Benutzer einer class oder function aufgefordert wird, Implementierungsdetails zu kennen und zu duplizieren. Wenn sich diese Implementierungsdetails ändern, kann Code, der Forward deklariert, unterbrochen werden, während Code, der auf dem Header basiert, weiterhin funktionieren wird.

    Weiterleiten einer function deklarieren:

    • erfordert, dass man weiß, dass es als function und nicht als Instanz eines statischen Funktorobjekts implementiert ist oder (keuch!) ein Makro,

    • erfordert das Duplizieren der Standardwerte für Standardparameter,

    • benötigt den tatsächlichen Namen und Namespace, da es möglicherweise nur eine using Deklaration ist, die es in einen anderen Namespace zieht, vielleicht unter einem Alias, und

    • kann die Inline-Optimierung verlieren.

    Wenn sich der konsumierende Code auf den Header stützt, können alle diese Implementierungsdetails vom functionsanbieter geändert werden, ohne dass der Code beschädigt wird.

    Weiterleiten einer class deklarieren:

    • erfordert, zu wissen, ob es sich um eine abgeleitete class und die Basisklasse (n) handelt, von denen sie abgeleitet ist,

    • erfordert, zu wissen, dass es sich um eine class und nicht nur um eine Typdefinition oder eine bestimmte Instanz einer classnvorlage handelt (oder zu wissen, dass es sich um eine classnvorlage handelt und alle Vorlagenparameter und Standardwerte korrekt sind),

    • erfordert, dass Sie den tatsächlichen Namen und Namespace der class kennen, da es sich um eine using Deklaration handeln kann, die sie in einen anderen Namespace, möglicherweise unter einem Aliasnamen, und

    • erfordert die Kenntnis der richtigen Attribute (vielleicht hat es spezielle Ausrichtung Anforderungen).

    Durch die Vorwärtsdeklaration wird die Verkapselung dieser Implementierungsdetails unterbrochen, wodurch Ihr Code anfälliger wird.

    Wenn Sie Header-Abhängigkeiten abschneiden müssen, um die Kompilierungszeit zu verkürzen, müssen Sie den Provider der class / function / library dazu bringen, einen speziellen Header für Forward-Deklarationen bereitzustellen. Die Standardbibliothek tut dies mit . Dieses Modell bewahrt die Kapselung von Implementierungsdetails und gibt dem Bibliotheksbetreuer die Möglichkeit, diese Implementierungsdetails zu ändern, ohne den Code zu unterbrechen, während gleichzeitig die Belastung des Compilers reduziert wird.

    Eine andere Option ist die Verwendung eines Pimpl-Idioms, das die Implementierungsdetails noch besser versteckt und die Kompilierung auf Kosten eines geringen Laufzeitaufwands beschleunigt.

    Gibt es irgendeinen Grund, warum das nicht zu tun, wo immer es möglich ist?

    Der einzige Grund, an den ich denke, ist, ein wenig Tipparbeit zu sparen.

    Ohne Forward-Deklarationen können Sie die Header-Datei nur einmal hinzufügen, aber ich rate nicht, dies bei irgendwelchen großen Projekten zu tun, aufgrund von Nachteilen, auf die andere Leute hingewiesen haben.

    Gibt es irgendeinen Grund, warum das nicht zu tun, wo immer es möglich ist?

    Ja – performance. classnobjekte werden mit ihren Datenelementen zusammen im Speicher gespeichert. Wenn Sie pointers verwenden, wird der Speicher für das tatsächliche Objekt, auf das verwiesen wird, an einer anderen Stelle auf dem Heap gespeichert, normalerweise weit entfernt. Dies bedeutet, dass der Zugriff auf das Objekt einen Cache-Fehler verursacht und neu geladen wird. Dies kann einen großen Unterschied in Situationen machen, in denen performance entscheidend ist.

    Auf meinem PC läuft die Faster () function ca. 2000x schneller als die Slower () function:

     class SomeClass { public: void DoSomething() { val++; } private: int val; }; class UsesPointers { public: UsesPointers() {a = new SomeClass;} ~UsesPointers() {delete a; a = 0;} SomeClass * a; }; class NonPointers { public: SomeClass a; }; #define ARRAY_SIZE 100000 void Slower() { UsesPointers list[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { list[i].a->DoSomething(); } } void Faster() { NonPointers list[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { list[i].a.DoSomething(); } } 

    In Teilen von Anwendungen, die performancekritisch sind, oder wenn an Hardware gearbeitet wird, die besonders anfällig für Cache-Kohärenzprobleme ist, können Datenlayout und -verwendung einen großen Unterschied machen.

    Dies ist eine gute Präsentation zum Thema und anderen performancesfaktoren: http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf