Sollte man nach Möglichkeit Forward-Deklarationen anstelle von Includes verwenden?

78

Wenn eine Klassendeklaration eine andere Klasse nur als Zeiger verwendet, ist es sinnvoll, eine Klassenvorwärtsdeklaration zu verwenden, anstatt die Headerdatei einzuschließen, um Probleme mit zirkulären Abhängigkeiten vorbeugend zu vermeiden? also anstatt zu haben:

//file C.h
#include "A.h"
#include "B.h"

class C{
    A* a;
    B b;
    ...
};

Tun Sie dies stattdessen:

//file C.h
#include "B.h"

class A;

class C{
    A* a;
    B b;
    ...
};


//file C.cpp
#include "C.h"
#include "A.h"
...

Gibt es einen Grund, dies nicht zu tun, wo immer dies möglich ist?

Matte
quelle
5
ähm - ist das die Antwort auf die Frage oben oder unten?
Mat
1
Ihre eigentliche Frage (unten) - AFAIK Es gibt keinen Grund, in diesem Fall keine Vorwärtserklärung zu verwenden ...
Nim
13
Es hängt leicht davon ab, was Sie unter "verwendet eine andere Klasse nur als Zeiger" verstehen. Es gibt einen bösen Fall, in dem Sie deleteeinen Zeiger nur mit einer Vorwärtsdeklaration verwenden können. Wenn die Klasse jedoch tatsächlich einen nicht trivialen Destruktor hat, erhalten Sie UB. Wenn also delete"nur Zeiger verwendet", dann gibt es einen Grund. Wenn es nicht zählt, nicht so sehr.
Steve Jessop
Ist die zirkuläre Abhängigkeit nicht noch vorhanden und nur vor dem Compiler verborgen? Wenn ja, lehren Sie beide Taktiken, die immer eine Vorwärtsdeklaration beinhalten und immer durchführen, nicht, wie Sie zirkuläre Abhängigkeiten vermeiden können. Ich muss jedoch zugeben, dass sie mit Vorwärtserklärungen möglicherweise leichter zu finden sind.
Grenix
1
Wenn die Klasse A tatsächlich als Struktur definiert ist, können sich einige Compiler beschweren. Wenn Klasse A eine abgeleitete Klasse ist, haben Sie Probleme. Wenn die Klasse A in einem anderen Namespace definiert ist und der Header sie nur mit einer using-Deklaration in diesen Namespace zieht, können Probleme auftreten. Wenn Klasse A tatsächlich ein Alias ​​(oder ein Makro!) Ist, haben Sie Probleme. Wenn Klasse A tatsächlich ein Typedef ist, haben Sie Probleme. Wenn Klasse A tatsächlich eine Klassenvorlage mit Standardvorlagenparametern ist, treten Probleme auf. Ja, es gibt einen Grund, die Deklaration nicht weiterzuleiten: Die Kapselung von Implementierungsdetails wird unterbrochen.
Adrian McCarthy

Antworten:

60

Die Vorwärtsdeklarationsmethode ist fast immer besser. (Ich kann mir keine Situation vorstellen, in der es besser ist, eine Datei einzuschließen, in der Sie eine Vorwärtsdeklaration verwenden können, aber ich werde nicht sagen, dass es für alle Fälle immer besser ist).

Es gibt keine Nachteile bei vorwärts deklarierten Klassen, aber ich kann mir einige Nachteile vorstellen, wenn Header unnötig eingefügt werden:

  • längere Kompilierungszeit, da alle Übersetzungseinheiten einschließlich C.henthalten A.h, obwohl sie diese 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

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

Luchian Grigore
quelle
11
Auch erhöhte Wahrscheinlichkeit der Neukompilierung.
Peter Wood
9
"Ich kann mir keine Situation vorstellen, in der es besser ist, eine Datei einzuschließen, in der Sie eine Vorwärtsdeklaration verwenden können" - wenn die Vorwärtsdeklaration UB ergibt, lesen Sie meinen Kommentar zur Hauptfrage. Sie haben Recht, vorsichtig zu sein, denke ich :-)
Steve Jessop
1
@Luchian: Weil es davon abhängt, was der Fragesteller ursprünglich gemeint hat, ob es sich um eine Antwort handelt oder nicht, möchte ich keine "Antworten" veröffentlichen, die sich auf Streit belaufen. Vielleicht würde der Fragesteller niemals davon träumen, eine deleteErklärung zu schreiben C.h.
Steve Jessop
2
Ein Nachteil ist mehr Arbeit und mehr Code! Und mehr Zerbrechlichkeit. Man kann unmöglich sagen, dass es keine Nachteile gibt.
usr
2
Was ist, wenn es 5 Klassen gibt? Was ist, wenn Sie später etwas hinzufügen müssen? Sie haben sich nur auf den bestmöglichen Fall für Ihren Punkt konzentriert.
usr
37

Ja, die Verwendung von Vorwärtsdeklarationen ist immer besser.

Einige der Vorteile, die sie bieten, sind:

  • Reduzierte Kompilierungszeit.
  • Keine Namespace-Verschmutzung.
  • (In einigen Fällen) kann die Größe Ihrer generierten Binärdateien verringern.
  • Die Neukompilierungszeit kann erheblich verkürzt werden.
  • Vermeiden möglicher Konflikte zwischen Präprozessornamen.
  • Durch die Implementierung von PIMPL Idiom können Sie die Implementierung vor der Schnittstelle verbergen.

Durch das Vorwärtsdeklarieren einer Klasse wird diese bestimmte Klasse jedoch zu einem unvollständigen Typ, und dies schränkt die Operationen, die Sie für den unvollständigen Typ ausführen können, erheblich ein.
Sie können keine Operationen ausführen, bei denen der Compiler das Layout der Klasse kennen müsste.

Mit Unvollständiger Typ können Sie:

  • Deklarieren Sie ein Mitglied als Zeiger oder Verweis auf den unvollständigen Typ.
  • Deklarieren Sie Funktionen oder Methoden, die unvollständige Typen akzeptieren / zurückgeben.
  • Definieren Sie Funktionen oder Methoden, die Zeiger / Verweise auf den unvollständigen Typ akzeptieren / zurückgeben (jedoch ohne Verwendung seiner Elemente).

Mit unvollständigem Typ können Sie nicht:

  • Verwenden Sie es als Basisklasse.
  • Verwenden Sie es, um ein Mitglied zu deklarieren.
  • Definieren Sie Funktionen oder Methoden mit diesem Typ.
Alok Speichern
quelle
1
"Durch das Vorwärtsdeklarieren einer Klasse wird diese bestimmte Klasse jedoch zu einem unvollständigen Typ, und dies schränkt die Operationen, die Sie für den unvollständigen Typ ausführen können, erheblich ein." Nun ja, aber wenn Sie es weiterleiten können, bedeutet dies, dass Sie keinen vollständigen Typ in der Kopfzeile benötigen. Wenn Sie einen vollständigen Typ in einer Datei benötigen, die diesen Header enthält, geben Sie einfach den Header für den gewünschten Typ an. IMO, das ist ein Vorteil - es zwingt Sie, alles, was Sie brauchen, in Ihre Implementierungsdatei aufzunehmen und sich nicht darauf zu verlassen, dass es woanders aufgenommen wird.
Luchian Grigore
Angenommen, jemand ändert diesen Header und ersetzt das Include durch eine Forward-Deklaration. Dann müssten Sie alle Dateien ändern, die diesen Header enthalten, den fehlenden Typ verwenden, aber den Header des fehlenden Typs selbst nicht einschließen (obwohl dies der Fall sein sollte).
Luchian Grigore
1
@LuchianGrigore: ..aber wenn Sie es weiterleiten können , müssen Sie es zum Auschecken versuchen. Es gibt also keine feste Regel, um nur Weiterleitungsdeklarationen zu verwenden und keine Header einzuschließen, da die Regeln die Organisation Ihrer Implementierungen unterstützen Die häufigste Verwendung von Forward-Deklarationen besteht darin, zirkuläre Abhängigkeiten aufzubrechen. Hier können Sie normalerweise nicht mit unvollständigen Typen umgehen. Jede Quelldatei und Header-Datei sollte alle für die Kompilierung erforderlichen Header enthalten, damit das zweite Argument dies nicht tut Es ist einfach ein schlecht organisierter Code.
Alok Save
1
Für PIMPL kann es auch sinnvoll sein, diese Vorwärtsdeklarationen nur in den privaten Teilen Ihrer Klasse zu verwenden
Grenix
21

Gibt es einen Grund, dies nicht zu tun, wo immer dies möglich ist?

Bequemlichkeit.

Wenn Sie vor der Phase wissen, dass jeder Benutzer dieser Header-Datei unbedingt auch die Definition von " Airgendetwas tun" (oder meistens) angeben muss. Dann ist es bequem, es nur ein für alle Mal einzuschließen.

Dies ist ein ziemlich heikles Thema, da eine zu liberale Verwendung dieser Daumenregel einen nahezu unkompilierbaren Code ergibt. Beachten Sie, dass Boost das Problem anders angeht, indem es spezielle "Convenience" -Header bereitstellt, die einige enge Funktionen miteinander bündeln.

Matthieu M.
quelle
5
Dies ist die einzige Antwort, die darauf hinweist, dass dies Produktivitätskosten verursacht. +1
usr
Aus der Sicht eines Benutzers. Wenn Sie alles deklarieren, bedeutet dies, dass der Benutzer diese Datei nicht einfach einschließen und sofort mit der Arbeit beginnen kann. Sie müssen herausfinden, um welche Abhängigkeiten es sich handelt (wahrscheinlich weil sich der Compiler über unvollständige Typen beschwert) und diese Dateien einschließen, bevor sie Ihre Klasse (n) verwenden können. Eine Alternative besteht darin, eine "shared.hpp" -Datei oder etwas für Ihre Bibliothek zu erstellen, in der sich alle Header in dieser Datei befinden (wie oben erwähnt). Sie können es leicht einschließen, ohne herauszufinden, warum sie nicht einfach "einschließen und gehen" können.
Todd
11

Ein Fall, in dem Sie keine Vorwärtsdeklarationen haben möchten, ist, wenn sie selbst schwierig sind. Dies kann passieren, wenn einige Ihrer Klassen Vorlagen enthalten, wie im folgenden Beispiel:

// Forward declarations
template <typename A> class Frobnicator;
template <typename A, typename B, typename C = Frobnicator<A> > 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<int, float>*);

Vorwärtsdeklarationen sind dasselbe wie Codeduplizierung: Wenn sich der Code häufig ändert, müssen Sie ihn jedes Mal an zwei oder mehr Stellen ändern, und das ist nicht gut.

Anatolyg
quelle
2
+1 für die Zerstörung des Konsenses, dass die Vorwärtsdeklaration immer besser ist :-) IIRC das gleiche Problem tritt bei Typen auf, die "heimlich" Template-Instanziierungen sind, über typedefs. namespace std { class string; }wäre falsch, selbst wenn es erlaubt wäre, die Klassendeklaration in den Namespace std zu setzen, weil (ich denke) Sie ein typedef nicht legal vorwärts deklarieren können, als ob es eine Klasse wäre.
Steve Jessop
8

Sollte man nach Möglichkeit Forward-Deklarationen anstelle von Includes verwenden?

Nein, explizite Vorwärtserklä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 fehleranfällig sein.

Um Nichtübereinstimmungen zwischen den "Vorwärts" -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 Klasse vorwärts deklariert ist, ist diese Vorwärtsdeklaration möglicherweise in Ordnung, aber im Allgemeinen kann "Vorwärtsdeklarationen anstelle von Includes verwenden, wann immer dies möglich ist", wie der Titel dieses Threads besagt sei ziemlich riskant.

Hier einige Beispiele für "unsichtbare Risiken" in Bezug auf Vorwärtsdeklarationen (unsichtbare Risiken = Deklarationsfehlanpassungen, 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 die korrekte Kenntnis des Footprints (der Größe) des Datentyps erfordern.

  • Explizite Vorwärtsdeklarationen von Symbolen, die Funktionen darstellen, können ebenso unsicher sein wie die Parametertypen und die Anzahl der Parameter.

Das folgende Beispiel veranschaulicht dies, z. B. zwei gefährliche Vorwärtsdeklarationen von Daten sowie einer Funktion:

Datei ac:

#include <iostream>
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 <iostream>
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 a.c b.c

Hinweis: Unsichtbare Gefahr, da g ++ keine Compiler- oder
Linkerfehler / Warnungen ausgibt. Hinweis: Das Weglassen extern "C"führt zu einem Verknüpfungsfehler function()aufgrund des Mangels des C ++ - Namens.

Ausführen des Programms:

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

Gibt es einen Grund, dies nicht zu tun, wo immer dies möglich ist?

Absolut: Die Kapselung wird unterbrochen, indem der Benutzer einer Klasse oder Funktion die Implementierungsdetails kennen und duplizieren muss. Wenn sich diese Implementierungsdetails ändern, kann Code, der weiterleitet, beschädigt werden, während Code, der auf dem Header basiert, weiterhin funktioniert.

Vorwärts deklarieren einer Funktion:

  • setzt voraus, dass es als Funktion und nicht als Instanz eines statischen Funktorobjekts oder (keuchend!) eines Makros implementiert ist.

  • erfordert das Duplizieren der Standardwerte für Standardparameter.

  • erfordert die Kenntnis des tatsächlichen Namens und des Namespace, da es sich möglicherweise nur um eine usingDeklaration handelt, die ihn in einen anderen Namespace zieht, möglicherweise unter einem Alias, und

  • kann bei der Inline-Optimierung verlieren.

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

Vorwärts deklarieren einer Klasse:

  • erfordert das Wissen, ob es sich um eine abgeleitete Klasse handelt und von welcher Basisklasse (n) sie abgeleitet ist.

  • setzt voraus, dass Sie wissen, dass es sich um eine Klasse handelt und nicht nur um ein typedef oder eine bestimmte Instanziierung einer Klassenvorlage (oder dass Sie wissen, dass es sich um eine Klassenvorlage handelt und dass alle Vorlagenparameter und Standardwerte korrekt sind).

  • erfordert die Kenntnis des wahren Namens und Namespace der Klasse, da es sich möglicherweise um eine usingDeklaration handelt, die sie in einen anderen Namespace zieht, möglicherweise unter einem Alias, und

  • erfordert die Kenntnis der richtigen Attribute (möglicherweise gelten spezielle Ausrichtungsanforderungen).

Wiederum unterbricht die Vorwärtsdeklaration die Kapselung dieser Implementierungsdetails, wodurch Ihr Code anfälliger wird.

Wenn Sie Header-Abhängigkeiten reduzieren müssen, um die Kompilierungszeit zu verkürzen, lassen Sie den Anbieter der Klasse / Funktion / Bibliothek einen speziellen Header für Vorwärtsdeklarationen bereitstellen. Die Standardbibliothek macht dies mit<iosfwd> . Dieses Modell bewahrt die Kapselung von Implementierungsdetails und gibt dem Bibliotheksverwalter die Möglichkeit, diese Implementierungsdetails zu ändern, ohne Ihren Code zu beschädigen, während gleichzeitig die Belastung des Compilers verringert wird.

Eine andere Möglichkeit ist die Verwendung eines Pimpl-Idioms, das Implementierungsdetails noch besser verbirgt und Kompilierungen auf Kosten eines geringen Laufzeitaufwands beschleunigt.

Adrian McCarthy
quelle
Am Ende empfehlen Sie die Verwendung der Pimpl-Redewendung, aber die gesamte Nützlichkeit dieser Redewendung basiert auf Vorwärtsdeklarationen. Forward Declarations und Pimpl Idiom sind so ziemlich dasselbe.
user2445507
@ user2445507: Die Frage war "wann immer möglich". Mein Punkt ist, dass es normalerweise keine gute Idee ist, "wann immer möglich" zu tun. Wie ich in meinem vorletzten Absatz sagte, ist es für den Eigentümer der Schnittstelle in Ordnung, Weiterleitungsdeklarationen bereitzustellen, da diese die Weiterleitungsdeklarationen mit der tatsächlichen Schnittstelle synchron halten können. Mit der Pimpl-Sprache ist derselbe Programmierer für die Vorwärtsdeklaration und die Implementierung des Impl-Objekts verantwortlich. Das ist also vollkommen in Ordnung.
Adrian McCarthy
2

Gibt es einen Grund, dies nicht zu tun, wo immer dies möglich ist?

Der einzige Grund, an den ich denke, ist, etwas Tippen zu sparen.

Ohne Vorwärtsdeklarationen können Sie die Header-Datei nur einmal einfügen, aber ich rate nicht dazu, dies bei ziemlich großen Projekten zu tun, da andere Personen auf Nachteile hinweisen.

ks1322
quelle
1
@ Luchian Grigore: wahrscheinlich ist es in Ordnung für ein einfaches Testprogramm
ks1322
-1

Gibt es einen Grund, dies nicht zu tun, wo immer dies möglich ist?

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

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

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 leistungskritisch sind oder an Hardware arbeiten, die besonders anfällig für Cache-Kohärenzprobleme ist, können Datenlayout und -nutzung einen großen Unterschied machen.

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

Edward
quelle
9
Sie beantworten eine andere Frage ("Soll ich Zeiger verwenden?"), Nicht die, die gestellt wurde ("Wenn ich nur Zeiger verwende, gibt es einen Grund, keine Vorwärtsdeklarationen zu verwenden?").
niemand
@ AndrewMedico Ich denke, dies ist eine gute Antwort und weist auf den Leistungsnachteil hin, der die Frage beantwortet hat. "Vorwärtsdeklaration == mit Zeiger"
Eric