Warum verhindern meine Include-Wachen nicht die rekursive Aufnahme und die Definition mehrerer Symbole?

73

Zwei häufig gestellte Fragen zu gehören Wachen :

  1. ERSTE FRAGE:

    Warum schützen Include Guards meine Header-Dateien nicht vor gegenseitiger, rekursiver Aufnahme ? Ich erhalte jedes Mal Fehler über nicht vorhandene Symbole, die offensichtlich vorhanden sind, oder sogar seltsamere Syntaxfehler, wenn ich Folgendes schreibe:

    "Ah"

    #ifndef A_H
    #define A_H
    
    #include "b.h"
    
    ...
    
    #endif // A_H
    

    "bh"

    #ifndef B_H
    #define B_H
    
    #include "a.h"
    
    ...
    
    #endif // B_H
    

    "main.cpp"

    #include "a.h"
    int main()
    {
        ...
    }
    

    Warum erhalte ich Fehler beim Kompilieren von "main.cpp"? Was muss ich tun, um mein Problem zu lösen?


  1. ZWEITE FRAGE:

    Warum verhindern Include Guards nicht mehrere Definitionen ? Wenn mein Projekt beispielsweise zwei Dateien enthält, die denselben Header enthalten, beschwert sich der Linker manchmal darüber, dass ein Symbol mehrmals definiert wurde. Zum Beispiel:

    "header.h"

    #ifndef HEADER_H
    #define HEADER_H
    
    int f()
    {
        return 0;
    }
    
    #endif // HEADER_H
    

    "source1.cpp"

    #include "header.h"
    ...
    

    "source2.cpp"

    #include "header.h"
    ...
    

    Warum passiert das? Was muss ich tun, um mein Problem zu lösen?

Andy Prowl
quelle
3
Ich sehe nicht, wie dies anders ist als stackoverflow.com/questions/553682/… und stackoverflow.com/questions/14425262/…
Luchian Grigore
1
@LuchianGrigore: Die ersten Fragen und Antworten beziehen sich nicht direkt auf Include-Wachen, oder zumindest erklärt IMO nicht, warum Include-Wachen Probleme mit Abhängigkeiten verursachen. Die zweite Frage behandelt eine der beiden Fragen (die zweite), jedoch weniger ausführlich und detailliert. Ich wollte diese beiden Fragen und Antworten zu Wachen zusammenfassen, weil es mir so scheint, als ob sie eng miteinander verbunden sind.
Andy Prowl
1
@sbi: Mir geht es gut, wenn du das Tag entfernst, kein Problem. Ich denke nur, dass es eine häufig gestellte Frage zu C ++ ist, die als faq-c ++ gekennzeichnet werden sollte.
Andy Prowl
1
@sbi: Nun, in den letzten Tagen habe ich mindestens 4 Fragen zu SO von Anfängern gesehen, die durch mehrere Definitionen oder gegenseitige Einschlüsse verwirrt waren. Aus meiner Sicht ist es also eine wiederkehrende Frage. Deshalb habe ich mir überhaupt die Mühe gemacht, diese ganze Sache zu schreiben: Warum sollte ich sonst ein Q & A für Anfänger schreiben? Aber natürlich verstehe ich, dass jeder eine subjektive Wahrnehmung dessen hat, was "häufig" ist, und meine Wahrnehmung möglicherweise nicht mit Ihrer übereinstimmt. Obwohl ich immer noch der Meinung bin, dass dies als c ++ - faq gekennzeichnet werden sollte, habe ich nichts gegen einen Benutzer mit höheren Wiederholungszahlen mit mehr Erfahrung, um seine Ansicht durchzusetzen.
Andy Prowl
1
scheint mir eine FAQ zu sein
Jonathan Wakely

Antworten:

130

ERSTE FRAGE:

Warum schützen Include Guards meine Header-Dateien nicht vor gegenseitiger, rekursiver Aufnahme ?

Sie sind .

Was sie nicht unterstützen, sind Abhängigkeiten zwischen den Definitionen von Datenstrukturen in sich gegenseitig einschließenden Headern . Um zu sehen, was dies bedeutet, beginnen wir mit einem grundlegenden Szenario und sehen, warum Include-Wachen bei gegenseitigen Einschlüssen helfen.

Angenommen, Ihre sich gegenseitig einschließenden a.hund b.hHeader-Dateien haben einen trivialen Inhalt, dh die Ellipsen in den Codeabschnitten aus dem Text der Frage werden durch die leere Zeichenfolge ersetzt. In dieser Situation wird Ihr main.cppWille gerne kompiliert. Und das nur dank Ihrer Wachen!

Wenn Sie nicht überzeugt sind, entfernen Sie sie:

//================================================
// a.h

#include "b.h"

//================================================
// b.h

#include "a.h"

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

Sie werden feststellen, dass der Compiler einen Fehler meldet, wenn er die Einschluss-Tiefengrenze erreicht. Diese Grenze ist implementierungsspezifisch. Gemäß Abschnitt 16.2 / 6 des C ++ 11-Standards:

Eine # include-Vorverarbeitungsanweisung kann in einer Quelldatei angezeigt werden, die aufgrund einer # include-Anweisung in einer anderen Datei gelesen wurde, bis zu einem durch die Implementierung definierten Verschachtelungslimit .

Also, was ist los ?

  1. Beim Parsen main.cpperfüllt der Präprozessor die Anweisung #include "a.h". Diese Anweisung weist den Präprozessor an, die Header-Datei zu verarbeiten a.h, das Ergebnis dieser Verarbeitung zu übernehmen und die Zeichenfolge #include "a.h"durch dieses Ergebnis zu ersetzen .
  2. Während der Verarbeitung a.herfüllt der Präprozessor die Anweisung #include "b.h", und es gilt der gleiche Mechanismus: Der Präprozessor verarbeitet die Header-Datei b.h, nimmt das Ergebnis seiner Verarbeitung und ersetzt die #includeAnweisung durch dieses Ergebnis.
  3. Bei der Verarbeitung b.hweist die Direktive #include "a.h"den Präprozessor an a.h, diese Direktive zu verarbeiten und durch das Ergebnis zu ersetzen.
  4. Der Präprozessor beginnt a.herneut mit dem Parsen , erfüllt die #include "b.h"Anweisung erneut und richtet einen potenziell unendlichen rekursiven Prozess ein. Beim Erreichen der kritischen Verschachtelungsebene meldet der Compiler einen Fehler.

Wenn Include-Wachen vorhanden sind , wird in Schritt 4 jedoch keine unendliche Rekursion eingerichtet. Mal sehen, warum:

  1. ( wie zuvor ) Beim Parsen main.cpperfüllt der Präprozessor die Anweisung #include "a.h". Dies weist den Präprozessor an, die Header-Datei zu verarbeiten a.h, das Ergebnis dieser Verarbeitung zu übernehmen und die Zeichenfolge #include "a.h"durch dieses Ergebnis zu ersetzen .
  2. Während der Verarbeitung a.herfüllt der Präprozessor die Richtlinie #ifndef A_H. Da das Makro A_Hnoch nicht definiert wurde, wird der folgende Text weiter verarbeitet. Die nachfolgende Anweisung ( #defines A_H) definiert das Makro A_H. Dann erfüllt der Präprozessor die Anweisung #include "b.h": Der Präprozessor verarbeitet nun die Header-Datei b.h, nimmt das Ergebnis ihrer Verarbeitung und ersetzt die #includeAnweisung durch dieses Ergebnis.
  3. Bei der Verarbeitung b.herfüllt der Präprozessor die Richtlinie #ifndef B_H. Da das Makro B_Hnoch nicht definiert wurde, wird der folgende Text weiter verarbeitet. Die nachfolgende Anweisung ( #defines B_H) definiert das Makro B_H. Dann wird die Richtlinie #include "a.h"wird zeigen , den Präprozessor zu verarbeiten , a.hdie und ersetzt #includeRichtlinie b.hmit dem Ergebnis der Vorverarbeitung a.h;
  4. Der Compiler beginnt a.herneut mit der Vorverarbeitung und erfüllt die #ifndef A_HAnweisung erneut. Während der vorherigen Vorverarbeitung wurde jedoch ein Makro A_Hdefiniert. Daher überspringt der Compiler dieses Mal den folgenden Text, bis die übereinstimmende #endifDirektive gefunden ist und die Ausgabe dieser Verarbeitung die leere Zeichenfolge ist (vorausgesetzt, der #endifDirektive folgt natürlich nichts ). Der Präprozessor ersetzt daher die #include "a.h"Direktive in b.hdurch die leere Zeichenfolge und verfolgt die Ausführung zurück, bis die ursprüngliche #includeDirektive in ersetzt wird main.cpp.

So schützen Include-Wachen vor gegenseitiger Inklusion . Sie können jedoch nicht bei Abhängigkeiten zwischen den Definitionen Ihrer Klassen in Dateien helfen, die sich gegenseitig einschließen:

//================================================
// a.h

#ifndef A_H
#define A_H

#include "b.h"

struct A
{
};

#endif // A_H

//================================================
// b.h

#ifndef B_H
#define B_H

#include "a.h"

struct B
{
    A* pA;
};

#endif // B_H

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

Angesichts der oben genannten Header main.cppwird nicht kompiliert.

Warum passiert das?

Um zu sehen, was los ist, reicht es aus, die Schritte 1 bis 4 erneut durchzugehen.

Es ist leicht zu erkennen, dass die ersten drei Schritte und der größte Teil des vierten Schritts von dieser Änderung nicht betroffen sind (lesen Sie sie einfach durch, um sich zu überzeugen). Am Ende von Schritt 4 passiert jedoch etwas anderes: Nachdem die #include "a.h"Direktive b.hdurch die leere Zeichenfolge ersetzt wurde, beginnt der Präprozessor, den Inhalt von b.hund insbesondere die Definition von zu analysierenB . Leider Berwähnt die Definition der Klasse A, die noch nie zuvor genau wegen der Inklusionswächter erfüllt wurde !

Das Deklarieren einer Mitgliedsvariablen eines Typs, der zuvor nicht deklariert wurde, ist natürlich ein Fehler, und der Compiler wird höflich darauf hinweisen.

Was muss ich tun, um mein Problem zu lösen?

Sie benötigen Vorwärtserklärungen .

Tatsächlich ist die Definition der Klasse Anicht erforderlich, um die Klasse zu definieren B, da ein Zeiger auf Aals Mitgliedsvariable und nicht als Objekt vom Typ deklariert wird A. Da Zeiger eine feste Größe haben, muss der Compiler weder das genaue Layout kennen Anoch seine Größe berechnen, um die Klasse richtig zu definieren B. Daher reicht es aus, die Klasse Ain vorwärts zu deklarierenb.h und den Compiler auf ihre Existenz aufmerksam zu machen:

//================================================
// b.h

#ifndef B_H
#define B_H

// Forward declaration of A: no need to #include "a.h"
struct A;

struct B
{
    A* pA;
};

#endif // B_H

Ihre main.cpp Wille wird jetzt sicherlich kompiliert. Ein paar Bemerkungen:

  1. Nicht nur die gegenseitige Einbeziehung zu brechen, indem die #includeRichtlinie durch eine Vorwärtserklärung in ersetzt b.hwurde, reichte aus, um die Abhängigkeit von Bon effektiv auszudrücken A: Die Verwendung von Vorwärtserklärungen, wann immer dies möglich / praktisch ist, wird auch als gute Programmierpraxis angesehen , da dies dazu beiträgt, unnötige Einschlüsse zu vermeiden Reduzierung der gesamten Kompilierungszeit. Nach Eliminierung der gegenseitigen Einbeziehung main.cppmuss jedoch auf #includebeide a.hund b.h(falls letzteres überhaupt benötigt wird) geändert werden , da dies b.hnicht mehr indirekt ist#include d durch a.h;
  2. Während eine Vorwärtsdeklaration der Klasse Afür den Compiler ausreicht, um Zeiger auf diese Klasse zu deklarieren (oder sie in einem anderen Kontext zu verwenden, in dem unvollständige Typen akzeptabel sind), können Zeiger auf A(zum Aufrufen einer Mitgliedsfunktion) dereferenziert oder deren Größe berechnet werden Unzulässige Operationen für unvollständige Typen: Wenn dies erforderlich ist, die vollständige DefinitionA muss dem Compiler zur Verfügung stehen. Dies bedeutet, dass die Header-Datei, die sie definiert, enthalten sein muss. Aus diesem Grunde Definitionen Klasse und die Umsetzung ihrer Mitgliederfunktionen in der Regel in eine Header - Datei geteilt werden und eine Implementierungsdatei für diese Klasse (Klasse Vorlagen sind eine Ausnahme von dieser Regel): Implementierungsdateien, die nie werden #includedurch andere Dateien im Projekt d kann sicher#includealle notwendigen Header, um Definitionen sichtbar zu machen. Header-Dateien hingegen werden keine #includeanderen Header-Dateien verwenden, es sei denn, sie müssen dies wirklich tun (z. B. um die Definition einer Basisklasse sichtbar zu machen) und verwenden, wann immer möglich / praktisch, Vorwärtsdeklarationen.

ZWEITE FRAGE:

Warum verhindern Include Guards nicht mehrere Definitionen ?

Sie sind .

Sie schützen Sie nicht vor mehreren Definitionen in separaten Übersetzungseinheiten . Dies wird auch in diesen Fragen und Antworten zu StackOverflow erläutert .

Um dies zu sehen, entfernen Sie die Include-Schutzvorrichtungen und kompilieren Sie die folgende, modifizierte Version von source1.cpp(oder source2.cpp, was wichtig ist):

//================================================
// source1.cpp
//
// Good luck getting this to compile...

#include "header.h"
#include "header.h"

int main()
{
    ...
}

Der Compiler wird sich hier sicherlich über f()eine Neudefinition beschweren . Das liegt auf der Hand: Die Definition wird zweimal aufgenommen! Die oben genannten source1.cpp werden jedoch ohne Probleme kompiliert, wennheader.h die richtigen Include-Schutzvorrichtungen enthält . Das wird erwartet.

Selbst wenn die Include-Wachen vorhanden sind und der Compiler Sie nicht mehr mit Fehlermeldungen belästigt, besteht der Linker darauf, dass beim Zusammenführen des aus der Kompilierung von source1.cppund erhaltenen Objektcodes mehrere Definitionen gefunden werden source2.cpp, und weigert sich, Ihre zu generieren ausführbar.

Warum passiert das?

Grundsätzlich wird jede .cppDatei (der Fachbegriff in diesem Zusammenhang ist Übersetzungseinheit ) in Ihrem Projekt separat und unabhängig zusammengestellt . Beim Parsen a.cpp Datei verarbeitet der Präprozessor alle #includeAnweisungen und erweitert alle Makroaufrufe, auf die er stößt. Die Ausgabe dieser reinen Textverarbeitung wird als Eingabe an den Compiler zur Übersetzung in Objektcode übergeben. Sobald der Compiler mit der Erstellung des Objektcodes für eine Übersetzungseinheit fertig ist, fährt er mit der nächsten fort, und alle Makrodefinitionen, die bei der Verarbeitung der vorherigen Übersetzungseinheit aufgetreten sind, werden vergessen.

Tatsächlich ist das Kompilieren eines Projekts mit nÜbersetzungseinheiten ( .cppDateien) wie das Ausführen desselben Programms (des Compilers) njedes Mal mit einer anderen Eingabe: Unterschiedliche Ausführungen desselben Programms teilen nicht den Status der vorherigen Programmausführung (en) ) . Somit wird jede Übersetzung unabhängig ausgeführt und die Präprozessorsymbole, die beim Kompilieren einer Übersetzungseinheit auftreten, werden beim Kompilieren anderer Übersetzungseinheiten nicht berücksichtigt (wenn Sie einen Moment darüber nachdenken, werden Sie leicht erkennen, dass dies tatsächlich ein wünschenswertes Verhalten ist).

Obwohl Include Guards Ihnen dabei helfen, rekursive gegenseitige Einschlüsse und redundante Einschlüsse desselben Headers in einer Übersetzungseinheit zu verhindern, können sie daher nicht erkennen, ob dieselbe Definition in verschiedenen enthalten ist Übersetzungseinheiten enthalten ist.

Wenn Sie jedoch den Objektcode zusammenführen, der aus der Kompilierung aller .cppDateien Ihres Projekts generiert wurde , sieht der Linker , dass dasselbe Symbol mehrmals definiert wird, und da dies gegen die Ein-Definition-Regel verstößt . Gemäß Abschnitt 3.2 / 3 des C ++ 11-Standards:

Jedes Programm muss genau eine Definition jeder Nicht-Inline- Funktion oder -Variable enthalten, die in diesem Programm verwendet wird. Keine Diagnose erforderlich. Die Definition kann explizit im Programm erscheinen, sie befindet sich im Standard oder in einer benutzerdefinierten Bibliothek oder ist (falls zutreffend) implizit definiert (siehe 12.1, 12.4 und 12.8). In jeder Übersetzungseinheit, in der sie verwendet wird, ist eine Inline-Funktion zu definieren .

Daher gibt der Linker einen Fehler aus und weigert sich, die ausführbare Datei Ihres Programms zu generieren.

Was muss ich tun, um mein Problem zu lösen?

Wenn Sie Ihre Funktionsdefinition in einer Header-Datei behalten möchten, die #includeaus mehreren Übersetzungseinheiten besteht (beachten Sie, dass kein Problem auftritt, wenn Ihr Header #includenur aus einer Übersetzungseinheit besteht), müssen Sie das inlineSchlüsselwort verwenden.

Andernfalls müssen Sie nur die Deklaration Ihrer Funktion behalten header.hund ihre Definition (Text) nur in einer separaten .cppDatei ablegen (dies ist der klassische Ansatz).

Das inlineSchlüsselwort stellt eine unverbindliche Anforderung an den Compiler dar, den Funktionskörper direkt an der Aufrufstelle einzubinden, anstatt einen Stapelrahmen für einen regulären Funktionsaufruf einzurichten. Obwohl der Compiler Ihre Anforderung nicht erfüllen inlinemuss, weist das Schlüsselwort den Linker an, mehrere Symboldefinitionen zu tolerieren. Gemäß Abschnitt 3.2 / 5 des C ++ 11-Standards:

Es kann mehr als eine Definition eines Klassentyps (Abschnitt 9), eines Aufzählungstyps (7.2), einer Inline-Funktion mit externer Verknüpfung (7.1.2), einer Klassenvorlage (Abschnitt 14) und einer nicht statischen Funktionsvorlage (14.5.6) geben. , statisches Datenelement einer Klassenvorlage (14.5.1.3), Elementfunktion einer Klassenvorlage (14.5.1.1) oder Vorlagenspezialisierung, für die einige Vorlagenparameter in einem Programm nicht angegeben sind (14.7, 14.5.5), vorausgesetzt, dass jeweils Definition erscheint in einer anderen Übersetzungseinheit und vorausgesetzt, die Definitionen erfüllen die folgenden Anforderungen [...]

Der obige Absatz listet grundsätzlich alle Definitionen auf, die üblicherweise in Header-Dateien abgelegt werden , da sie sicher in mehreren Übersetzungseinheiten enthalten sein können. Alle anderen Definitionen mit externer Verknüpfung gehören stattdessen in die Quelldateien.

Die Verwendung des staticSchlüsselworts anstelle des inlineSchlüsselworts führt auch zur Unterdrückung von Linkerfehlern, indem Ihre Funktion intern verknüpft wird , sodass jede Übersetzungseinheit eine private Kopie dieser Funktion (und ihrer lokalen statischen Variablen) enthält. Dies führt jedoch letztendlich zu einer größeren ausführbaren Datei, und die Verwendung von inlinesollte im Allgemeinen bevorzugt werden.

Eine alternative Möglichkeit, das gleiche Ergebnis wie mit dem staticSchlüsselwort zu erzielen, besteht darin, die Funktion f()in einen unbenannten Namespace zu stellen . Gemäß Abschnitt 3.5 / 4 des C ++ 11-Standards:

Ein unbenannter Namespace oder ein direkt oder indirekt in einem unbenannten Namespace deklarierter Namespace ist intern verknüpft. Alle anderen Namespaces sind extern verknüpft. Ein Name mit einem Namespace-Bereich, dem oben keine interne Verknüpfung zugewiesen wurde, hat dieselbe Verknüpfung wie der einschließende Namespace, wenn es sich um den Namen von:

- eine Variable; oder

- eine Funktion ; oder

- eine benannte Klasse (Abschnitt 9) oder eine unbenannte Klasse, die in einer typedef-Deklaration definiert ist, in der die Klasse den typedef-Namen für Verknüpfungszwecke hat (7.1.3); oder

- eine benannte Aufzählung (7.2) oder eine unbenannte Aufzählung, die in einer typedef-Deklaration definiert ist, in der die Aufzählung den typedef-Namen für Verknüpfungszwecke enthält (7.1.3); oder

- einen Enumerator, der zu einer Enumeration mit Verknüpfung gehört; oder

- eine Vorlage.

Aus dem oben genannten Grund sollte das inlineSchlüsselwort bevorzugt werden.

Andy Prowl
quelle
Nett. Irgendwo um die Diskussion der beiden ODR-Varianten herum möchte ich darauf hinweisen, dass das zitierte 3.2 / 3 die Definitionen auflistet, die wir üblicherweise in Header-Dateien einfügen, und alle anderen Definitionen mit externer Verknüpfung in Quelldateien gehören. Und dann eine Checkliste in einfacher Sprache für "Welche Art von ODR gilt für meine Definition?"
Aschepler
@aschepler: Meinen Sie 3.2 / 4 ( "Ein Typ T muss vollständig sein, wenn ..." ) oder vielmehr 3.2 / 5 ( "Es kann mehr als eine Definition eines Klassentyps geben (Abschnitt 9), Aufzählungstyp (7.2) ), Inline-Funktion mit externer Verknüpfung (7.1.2), Klassenvorlage (Abschnitt 14), [...] und vorausgesetzt, die Definitionen erfüllen die folgenden Anforderungen [...] ")? Ich denke, es wäre nützlich, beides zu erwähnen, andererseits wäre es schwierig, dies kurz genug zu tun, und mit einer langen Erklärung wird sich der Fokus weg von den Include-Wachen verlagern, die Gegenstand dieser Fragen und Antworten sind. Vielleicht ein neuer FAQ-Eintrag, der mit diesem verknüpft ist?
Andy Prowl
4
@AndyProwl - Die übliche Antwort darauf ist Soziopathie. Lass dich nicht unterkriegen. Toller Beitrag ... +1
Jim Balter
1
@ Andrew: Danke, ich bin froh, dass du die Energie gefunden hast: D
Andy Prowl
3
@ AndyProwl danke, dass Sie sich Zeit genommen und eine so hilfreiche und ausführliche Erklärung geschrieben haben, +1
v.tralala
-2

Zunächst sollten Sie zu 100% sicher sein, dass Sie keine Duplikate in "Include Guards" haben.

Mit diesem Befehl

grep -rh "#ifndef" * 2>&1 | uniq -c | sort -rn | awk '{print $1 " " $3}' | grep -v "^1\ "

Sie werden 1) alle Include-Schutzvorrichtungen markieren, eine eindeutige Zeile mit Zähler pro Include-Name erhalten, die Ergebnisse sortieren, nur den Zähler und den Include-Namen drucken und diejenigen entfernen, die wirklich eindeutig sind.

TIPP: Dies entspricht dem Abrufen der Liste der duplizierten Include-Namen

Fiorentinoing
quelle