[C++] Include oder Forward-Declaration? Richtlinien?

G

Green Mamba

Gast
Hi,

ich hatte gerade mit Kollegen eine Diskussion über Forward-Declarations und #includes. Ich habs für meinen Teil bislang immer so gehalten dass ich in Header-Files Forward-Declarations benutzt habe, und in der Klasse dann die entsprechenden Headers includiert habe. Gibts generelle Richtlinien, wann man was benutzt?
Irgendwie hat hier jeder seinen eigenen Stil. :rolleyes:
In welcher Reihenfolge arbeitet der Compiler die Includes eigentlich ab? Werden zuerst alle includes vor dem eigentlichen compilieren rekursiv aufgelöst, und das bei jeder Klasse aufs neue? Oder wie gehts genau? Hab damals bei Compilerbau gefehlt. :D

Viele Grüße,
Green Mamba
 
also die header werden mit dem präprozessor (nicht vom compiler selbst) ersetzt, also jeder #include wird durch den inhalt der entsprechenden datei ersetzt. wenn man also sehr große header-files mit einer hohen anzahl an andere headerfiles darin hat, ist es eine erheblicher performance vorteil bei compilieren, wenn man mit forwarding arbeitet. merkt man z.b. bei qt sehr stark.
eine andere möglichkeit sind precompiled-header. bringen auch vorteile, wenn das eigene system dieses unterstützt.
also ich bevorzuge so weit möglich forwarding.
 
Naja, das ist jetzt ein bisschen Begriffsverwischung :) Mit "in der Klasse" meinst du wohl "in der .cpp-Datei", oder?


ghorst hat es schon gesagt, der Präprozessor macht für jede .cpp eine entsprechende Datei, wo er alle includes "reinkleistert", alle Makros expandiert etc.

Machen wir mal ein simples Beispiel:

a.h:
... irgendwas

b.h:
#include "a.h"

main.cpp
#include "a.h"
#include "b.h"


Dann wird jetzt beim Kompilieren von main.cpp zuerst #include "a.h" expandiert und der Header dort reinkopiert. Jetzt wird b.h expandiert, welches selbst erstmal wieder a.h expandieren will. Da wir in a.h zum Glück include-Guards verwendet haben, expandiert sich a.h in b.h nicht mehr (sonst gäb's mehrfache Definitionen, was eine Verletzung der ODR (One Definition Rule) wäre). Damit steht dann in der generierten main.cpp zuerst der Inhalt von a.h, dann b.h, dann main.cpp. (Man sieht übrigens: main.cpp hängt von a.h und b.h ab, muss also bei einer Änderung in a.h oder b.h neu kompiliert werden)
Soviel dazu.


Die Frage ob forward oder include: Include nur dann, wenn es wirklich unbedingt benötigt wird - sonst forward.

Beispiel für einen schlechten Header:

a.h
Code:
#ifndef A_H
#define A_H

#include "b.h" // für Klasse B
#include "c.h" // für Klasse C
#include "d.h" // für Klasse D
#include "e.h" // für Klasse E

#include <iostream>

class A : public E
{
public:
  A( B*);

  void doSomething( const D&);
private:
  C c_;
};

std::ostream& operator<<( std::ostream&, const A&);
#endif

Was ist an diesem Header schlecht? Wer ihn inkludiert, hängt von e.h, b.h, c.h und d.h ab und muss zusätzlich das include-Monster iostream einbinden, auch wenn man sich überhaupt nicht für den operator<< interessiert (sprich ihn nicht verwendet). Includiert man a.h, wird man auch bei einer Änderung von b.h, c.h, d.h und e.h neu kompiliert.

Nur das #include "e.h" und #include "c.h" wird hier wirklich benötigt. Wobei sich das #include "c.h" sogar vermeiden ließe.

Warum ist das so? Fangen wir mit dem Einfachen an: Wir leiten von E ab, also muss die Definition von E vollständig bekannt sein.

Der Konstruktor von A bekommt einen Pointer auf B. Zur Übergabe des Zeigers muss die Definition von B aber nicht bekannt sein. Es wird im Header nur der Zeiger verwendet, nicht B selbst. Damit reicht es, b.h durch eine Forwarddeklaration class B; zu ersetzen. Schwups, ein Header von dem wir nicht mehr abhängen.
Das selbe trifft auf die Methode doSomething zu, die eine const-Referenz auf D bekommt. D selbst wird bei der Übergabe nicht verwendet. #include "d.h" ist überflüssig - raus damit, Forward-Deklaration rein.

Nun zur Membervariable c_ vom Typ C. C ist ein Bestandteil von A. Ohne Vollständige Kenntnis von C können wir kein A definieren. Include muss sein. .... Wirklich? Wenn ich A benutzen will, interessiert mich doch nur seine öffentliche Schnittstelle und nicht irgendwelche Interna. Obwohl mich C nicht die Bohne interessiert und ich es nicht verwende, hänge ich von ihm ab. -> DOOF. In Anbetracht des obigen: Ändert man C c_; in C* c_; , so muss man nur einen Zeiger auf ein C halten - womit C im Header nicht mehr bekannt sein muss. Puh *Schweiß wegwisch*, wieder eine Abhängigkeit entfernt.
(Man sollte aber nicht den Nachteil dieser Technik verschweigen: Ab sofort muss man einen vernünftigen Zuweisungsoperator und Kopierkonstruktor zur Verfügung stellen, da der Compilergenerierte es nicht mehr sinnvoll tut. Außerdem muss man sich um das Löschen von C kümmern. Und der Zeitbedarf für die Allokierung mit new ist wie das Fahren mit angezogener Handbremse im Vergleich zum Stack)

Nun noch zum operator<< : Wenn ich A benutzen will, will ich es aber noch lange nicht auch ausgeben. Ich interessiere mich nicht für ihn und will auch nicht dieses superfette #include <iostream> da (iostream ist der größte Header der Standardbibliothek, bzw. zieht am meisten Abhängigkeiten mit sich). Vor dem C++ Standard konnten wir noch sowas schönes schreiben wie: class ostream; und wir waren aus dem Schneider. Mit dem Standard ist ostream aber in den Namensraum std gewandert. Okay, machen wir das hier:
Code:
namespace std { class ostream; }
Sieht gut aus, ist es aber nicht. ostream ist nämlich nur ein Typedef auf basic_ostream<char, char_traits<char> >. Dies wiederum dürfen wir aber nicht forward deklarieren (aus Gründen, die für alle STL-Klassen zutreffen). Zum Glück hat der Standard die armen Benutzer von <iostream> bedacht und den Header <iosfwd> spendiert, der Forward-Deklarationen auf alle Streamklassen definiert. Prima! <iostream> raus, <iosfwd> rein. (Übrigens: Auf andere STL-Klassen wie string und vector lassen sich keine standardkonformen Forwarddeklarationen hinschreiben)




Nun haben wir alle Abhängigkeiten aus dem Header eliminiert, die sich eliminieren lassen! Und das durch forward-Deklarationen. Deshalb sind Forward-Deklarationen Includes vorzuziehen.

Man sollte sich durch eine Sache nicht täuschen lassen. Wenn man z.B. jetzt den Operator << auch verwenden will, kommt man um das #include <iostream> nicht herum (dann natürlich das include nur in der .cpp Datei, wo er aufgerufen wird). Braucht man ihn aber garnicht, ist <iostream> unnötiger Ballast und <iosfwd> angebracht.
 
Zuletzt bearbeitet:
Wir haben hier die Begriffe Header und Klassendatei verwendet, gibts da bessere allgemeingültige Begriffe? Header und source?
Vielen Dank für die ausführliche Erklärung, Teile waren mir bekannt, aber Teile auch nicht (so richtig). Jetzt hat sich glaub ich ein besseres Gesamtverständnis gebildet. :)
Wenn ich jetzt mit C* c_ arbeite statt mit einer Referenz, dann muss ich, wenn ich C auch verwenden will im CPP-File, c.h includen. Dieses include schreib ich doch dann besser ins CPP-File, oder? Dadurch habe ich den Vorteil, dass andere Klassen die von A abhängen nicht auch gleich wieder von C abhängen, oder sehe ich das falsch?

Interessant ist dass ich es die ganze Zeit über instinktiv richtig gemacht habe. :cool_alt:
 
Zuletzt bearbeitet:
Siehst du genau richtig ;)

Und wer von A abhängt, kann aber nur die öffentliche Schnittstelle benutzen (es sei denn, man bastelt da friends rein oder so). Von außen kann man C also nie benutzen, muss es aber kennen, wenn es als Member der Klasse definiert ist.




Die konsequente Fortsetzung dieses Prinzips ist, die gesamte private Schnittstelle in eine extra Klasse zu verlagern, die in der .cpp-Datei definiert wird. Im Header gibt es nur einen Zeiger auf diese versteckte Klasse und die entsprechende Forward-Deklaration. Dieses Konzept nennt sich dann "Pimpl"-Idiom.

Man sollte jetzt nur nicht voller Euphorie dieses Idiom einsetzen, denn es hat genau die Nachteile, dass man einen Zuweisungsoperator, Kopierkonstruktor und Destruktor korrekt implementieren muss und das Erzeugen der versteckten Klasse mit new ziemlich teuer sein kann, vor allem, wenn man sehr viele Objekte davon anlegt. Außerdem kommt durch den Zeiger noch eine zusätzliche Indirektion (also Aufwand beim Aufrufen) hinzu. Man sollte also abwägen, was der Einsatz eines Pimpls bringt.
 
Vielen Dank nochmal für die wirklich gelungenen Erklärungen! :)
Über Pimpl-Idome hatte ich schonmal gelesen, das ist in manchen Fällen ein extrem nützliches Konstrukt.
 
Was ziemlich gefährliches bezüglich Forward-Deklarationen ist mir jetzt erst kürzlich untergekommen.

Das trifft aber nur auf Basisklassenzeiger zu und wenn man noch mit alten C-Casts rumpanscht.

Code:
class A;
class B; // B ist von A abgeleitet

void f( B* b)
{
   A* a = (A*) b;
   g( a);
}

void g( A* a)
{
   // irgendwas machen mit dem Zeiger
}

Sieht harmlos aus, ist es aber nicht. Auch wenn wir hier nur mit Zeigern hantieren, was ja eigentlich okay ist da wir die Objekte selbst nicht verwenden, verstecken wir etwas sehr wichtiges vor dem Compiler : Die Verwandschaftsbeziehung von A und B.

Ist die Verwandschaftsbeziehung bekannt, so passt er bei (A*)b automatisch den Zeiger entsprechend des Klassenlayouts an. Ist die Verwandschaftsbeziehung aber nicht bekannt, so wird b einfach in a reingepresst, auch wenn das absolut tödlich ist. Der Cast tut aber immernoch was zulässiges und es gibt keine Warnung und keine Chance, den Fehler zu entdecken.
(Dass das Layout angepasst werden muss, liegt an mehrfacher Vererbung und virtuellen Basisklassen)

Um solche Hässlichkeiten zu umgehen, sollte man eh grundsätzlich niemals C-Casts schreiben, sondern den entsprechenden C++-Cast. Also hier: A* a = static_cast<A*>(b); Sind dann die Definitionen von A und B nicht bekannt, gibt's einen Compile-Fehler.
 

Ähnliche Themen

H
Antworten
23
Aufrufe
9.750
H
Zurück
Oben