Die Verwendung von Double Include Guards in C ++

73

So hatte ich kürzlich eine Diskussion, in der ich arbeite, in der ich die Verwendung einer doppelten Einschlusswache über eine einzelne Wache in Frage stellte . Was ich mit Doppelwache meine , ist wie folgt:

Header-Datei "header_a.hpp":

#ifndef __HEADER_A_HPP__
#define __HEADER_A_HPP__
...
...
#endif

Wenn Sie die Header-Datei an einer beliebigen Stelle einfügen, entweder in eine Header- oder eine Quelldatei:

#ifndef __HEADER_A_HPP__
#include "header_a.hpp"
#endif

Jetzt verstehe ich, dass die Verwendung des Schutzes in Header-Dateien dazu dient, die mehrfache Aufnahme einer bereits definierten Header-Datei zu verhindern. Dies ist üblich und gut dokumentiert. Wenn das Makro bereits definiert ist, wird die gesamte Header-Datei vom Compiler als "leer" angesehen und die doppelte Einbeziehung wird verhindert. Einfach genug.

Das Problem, das ich nicht verstehe, ist die Verwendung von #ifndef __HEADER_A_HPP__und #endifum das #include "header_a.hpp". Der Mitarbeiter hat mir gesagt, dass dies den Einschlüssen eine zweite Schutzschicht hinzufügt, aber ich kann nicht erkennen, wie nützlich diese zweite Schicht überhaupt ist, wenn die erste Schicht den Job absolut erledigt (oder?).

Der einzige Vorteil, den ich mir einfallen lassen kann, ist, dass der Linker sich nicht mehr darum kümmert, die Datei zu finden. Soll dies die Kompilierungszeit verbessern (was nicht als Vorteil erwähnt wurde), oder ist hier noch etwas anderes am Werk, das ich nicht sehe?

sh3rifme
quelle
36
Dies fügt dem Code nur eine weitere Ebene der Sprödigkeit hinzu. Eine zweite Schicht ist völlig unnötig.
DeiDei
19
Nicht der Linker, sondern der Vorprozessor. Ehrlich gesagt scheint mir ein solcher Vorteil bei einem modernen Build-System vernachlässigbar, wenn Sie nur das einbeziehen, was Sie brauchen. Seine "Erklärung" erinnert eher an einen erfahrenen Anfänger, um ehrlich zu sein.
StoryTeller - Unslander Monica
18
Nacheinander gab es möglicherweise einen oder zwei Compiler, die dumm genug waren, die Datei jedes Mal zu öffnen, um den Include-Guard zu überprüfen. Kein in diesem Jahrtausend produzierter Compiler würde dies tun, da er nur eine Tabelle mit Dateien und Wachen einschließen und diese konsultieren kann, bevor er die Datei öffnet.
Bo Persson
7
Es ist völlig unnötig. Es gibt überhaupt keinen Vorteil.
Jabberwocky
34
Beachten Sie, dass Namen, die zwei aufeinanderfolgende Unterstriche ( __HEADER_A_HPP__) enthalten, und Namen, die mit einem Unterstrich gefolgt von einem Großbuchstaben beginnen, für die Implementierung reserviert sind. Verwenden Sie sie nicht in Ihrem Code.
Pete Becker

Antworten:

107

Ich bin mir ziemlich sicher, dass es eine schlechte Praxis ist, einen weiteren Include-Guard hinzuzufügen, wie:

#ifndef __HEADER_A_HPP__
#include "header_a.hpp"
#endif

Hier sind einige Gründe warum:

  1. Um eine doppelte Aufnahme zu vermeiden, reicht es aus, einen üblichen Include-Schutz in die Header-Datei selbst einzufügen. Es macht den Job gut. Ein weiterer Include-Schutz anstelle des Einschlusses bringt den Code nur durcheinander und verringert die Lesbarkeit.

  2. Es werden unnötige Abhängigkeiten hinzugefügt. Wenn Sie den Include Guard in der Header-Datei ändern, müssen Sie ihn an allen Stellen ändern, an denen der Header enthalten ist.

  3. Es ist definitiv nicht die teuerste Operation, die den gesamten Kompilierungs- / Verknüpfungsprozess vergleicht, sodass die gesamte Erstellungszeit kaum verkürzt werden kann.

  4. Jeder Compiler, der etwas wert ist, optimiert bereits dateiweite Include-Guards .

Edgar Rokjān
quelle
2
If you change include guard inside the header file you have to change it in all places where the header is included..... nun, technisch gesehen, nein, das tust du nicht, aber ich denke, das beweist den Punkt noch weiter.
txtechhelp
Ich habe Leute gesehen, die dies getan haben, als sie versucht haben, einige Probleme mit dem Oracle Pro-C-Vorprozessor zu umgehen. Mag es immer noch nicht.
user2038893
51

Der Grund für das Einfügen von Include-Schutzvorrichtungen in die Header- Datei besteht darin, zu verhindern, dass der Inhalt des Headers mehrmals in eine Übersetzungseinheit gezogen wird. Das ist eine normale, seit langem etablierte Praxis.

Der Grund für das Einfügen redundanter Include-Guards in eine Quelldatei besteht darin, dass die enthaltene Header-Datei nicht geöffnet werden muss, und dies in früheren Zeiten, die die Kompilierung erheblich beschleunigen könnten. Heutzutage ist das Öffnen einer Datei viel schneller als früher. Darüber hinaus sind Compiler ziemlich schlau darin, sich zu merken, welche Dateien sie bereits gesehen haben, und sie verstehen die Include-Guard-Sprache, sodass sie selbst herausfinden können, dass sie die Datei nicht erneut öffnen müssen. Das ist ein bisschen Handbewegung, aber unter dem Strich wird diese zusätzliche Ebene nicht mehr benötigt.

BEARBEITEN: Ein weiterer Faktor ist, dass das Kompilieren von C ++ weitaus komplizierter ist als das Kompilieren von C. Daher dauert es viel länger, sodass der Zeitaufwand für das Öffnen von Include-Dateien einen geringeren, weniger bedeutenden Teil der Zeit zum Kompilieren einer Übersetzungseinheit ausmacht.

Pete Becker
quelle
7
Hier ist ein Link, um Ihre "Handbewegung" mit einigen Unterlagen zu sichern ;-)
Arne Vogel
@ArneVogel Ich bemerkte die docs sagen: „Es muss außerhalb der Kontrolle keine Tokens sein #if- #endifPaar, aber Leerzeichen und Kommentare erlaubt sind.“ Beinhaltet "Token" #pragma once?
sh3rifme
3
@ sh3rifme Ja. Lesen Sie diesen Satz als „Das einzige , was Sie außerhalb der Kontrolle setzen können #if- #endifPaar , ohne die Optimierung zu deaktivieren sind Leerzeichen und Kommentare.“ Aber du solltest es #pragma oncesowieso nicht benutzen .
zwol
Nun, Sie könnten #pragma oncein den #ifndef/#endifBlock setzen. Wir verwenden es jedoch nicht, #pragma onceda einer der Compiler, die wir bei der Arbeit verwenden, es nicht unterstützt.
Tom Tanner
@ sh3rifme: #pragma onceist kein Token, sondern eine Präprozessor-Direktive. Diese sind jedoch auch nicht zulässig, damit die Optimierung funktioniert. Allerdings unterstützt GCC #pragma oncedaher die Optimierung und #pragma onceist redundant. Wie Tom Tanner vorgeschlagen hat, können Sie das Pragma genauso gut in den Block legen , wenn Sie beide verwenden #pragma once und Wachen einschließen #ifndef/#endif. In dem unwahrscheinlichen Fall, dass Ihr Compiler über die Optimierung mehrerer Includes verfügt #pragma once, diese jedoch nicht unterstützt , sollten Sie dies abdecken. Das #includeVerhalten ist ohnehin notorisch implementierungsabhängig.
Arne Vogel
22

Der einzige Vorteil, den ich mir einfallen lassen kann, ist, dass der Linker sich nicht mehr darum kümmert, die Datei zu finden.

Der Linker ist in keiner Weise betroffen.

Dies könnte verhindern, dass sich der Vorprozessor die Mühe macht, die Datei zu finden. Wenn der Schutz jedoch definiert ist, bedeutet dies, dass er die Datei bereits gefunden hat. Ich vermute, dass, wenn die Vorverarbeitungszeit überhaupt verkürzt wird, der Effekt ziemlich gering wäre, außer in der pathologisch rekursiv eingeschlossenen Monstrosität.

Es hat den Nachteil, dass, wenn die Wache jemals geändert wird (z. B. aufgrund eines Konflikts mit einer anderen Wache), alle Bedingungen vor den Include-Anweisungen geändert werden müssen, damit sie funktionieren. Und wenn etwas anderes den vorherigen Schutz verwendet, müssen die Bedingungen geändert werden, damit die Include-Direktive selbst ordnungsgemäß funktioniert.

PS __HEADER_A_HPP__ist ein Symbol, das der Implementierung vorbehalten ist. Sie können es also nicht definieren. Verwenden Sie einen anderen Namen für die Wache.

Eerorika
quelle
Entschuldigung für die Verwechslung mit dem Linker / Pre-Prozessor. Sie sagen, das __HEADER_A_HPP__ist der Implementierung vorbehalten. Was meinen Sie damit? Ist es speziell die Verwendung dieser Semantik, wie math.hppund __MATH_HPP__?
sh3rifme
13
@ sh3rifme Der Standard besagt, dass alle Bezeichner, die zwei aufeinanderfolgende Unterstriche enthalten, der Implementierung vorbehalten sind. Es gibt auch andere reservierte Bezeichner. Ich empfehle, sich mit diesen Regeln vertraut zu machen.
Eerorika
7
@ sh3rifme: Reserviert für die Implementierung, die Verwendungen enthalten kann, die automatisch definieren, __HEADER_A_HPP__wann Sie sie einschließen header_a.hpp. Dies unterbricht natürlich Ihren Header Guard, der davon ausgeht, dass er nur in der zweiten Zeile definiert ist.
MSalters
17

Ältere Compiler auf traditionelleren (Mainframe-) Plattformen (wir sprechen hier von Mitte der 2000er Jahre) hatten nicht die in anderen Antworten beschriebene Optimierung und verlangsamten daher die Vorverarbeitungszeit beim erneuten Lesen von Header-Dateien erheblich Diese wurden bereits aufgenommen (unter Berücksichtigung, dass Sie in einem großen, monolithischen Unternehmensprojekt eine Menge Header-Dateien einschließen werden). Als Beispiel habe ich Daten gesehen, die eine 26-fache Beschleunigung für eine Datei mit jeweils 256 Header-Dateien anzeigen, einschließlich derselben 256 Header-Dateien auf dem VisualAge C ++ 6 für AIX-Compiler (der Mitte der 2000er Jahre stammt). Dies ist ein ziemlich extremes Beispiel, aber diese Art der Beschleunigung summiert sich.

Alle modernen Compiler, selbst auf Mainframe-Plattformen wie AIX und Solaris, führen jedoch eine ausreichende Optimierung für die Einbeziehung von Headern durch, sodass der Unterschied heutzutage wirklich vernachlässigbar ist. Daher gibt es keinen guten Grund mehr, diese zu haben.

Dies erklärt jedoch, warum einige Unternehmen immer noch an der Praxis festhalten, da sie sich vor relativ kurzer Zeit (zumindest in Bezug auf das Alter der C / C ++ - Codebasis) für sehr große monolithische Projekte noch gelohnt hat.

Muzer
quelle
Ich erinnere mich, dass ich einmal einen IBM fortran-Compiler verwenden musste, der Schnecken wie Rennpferde aussehen ließ. Das Kompilieren einer Datei dauerte nicht weniger als eine halbe Stunde auf ziemlich leistungsfähiger Hardware. gfortranerledigte den gleichen Job innerhalb eines Bruchteils dieser Zeit. Vielleicht sind IBM Compiler nicht die beste Referenz für die Messung der Kompilierungsgeschwindigkeit. Auf einem modernen Kernel befinden sich diese 256 Header-Dateien immer noch im Seiten-Cache, wenn der Compiler versucht, sie zu lesen. Daher sollte das Öffnen von 64 KB für dieselben 256 Dateien nicht länger als eine Sekunde dauern, wenn ein Systemaufruf weniger als 10 Mikrosekunden beträgt .
cmaster - wieder herstellen Monica
1
@ Master nicht nur die Eröffnung, sondern auch das Lesen - denken Sie daran, der Präprozessor muss bis zum letzten
#endif
2
Welches ist auch noch im Seiten-Cache. Selbst wenn jede der 256 Dateien eine Größe von 128 KB hat, sind das nur 32 MB Daten, was insgesamt 8 GB Daten entspricht, die vom Kernel-Speicherplatz in den Benutzerbereich kopiert werden müssen. Moderne Hardware schafft das in weniger als einer Sekunde. Wenn ein Compiler bei diesem Vorgang schmerzhaft lange braucht, ist der Compiler zu 100% schuld.
cmaster - wieder einsetzen Monica
8

Obwohl es Leute gibt, die dagegen argumentieren, funktioniert '#pragma einmal' in der Praxis perfekt und die Hauptcompiler (gcc / g ++, vc ++) unterstützen es.

Was auch immer puristische Argumentation verbreitet, es funktioniert viel besser:

  1. Schnell
  2. Keine Wartung, keine Probleme mit mysteriöser Nichteinbeziehung, da Sie eine alte Flagge kopiert haben
  3. Einzelne Zeile mit offensichtlicher Bedeutung im Vergleich zu kryptischen Zeilen, die in der Datei verteilt sind

Also einfach gesagt:

#pragma once

am Anfang der Datei, und das war's. Optimiert, wartbar und einsatzbereit.

Bert Bril
quelle
1
Include-Wachen sind nicht kryptisch. Jeder C (++) - Programmierer, der weiß, was er tut, wird einen Include-Guard sofort verstehen, und weniger erfahrene müssen möglicherweise sogar nachschlagen #pragma once(während ein Standard-Include-Guard in Ordnung wäre). Da alle Compiler entweder den Include Guard (echte Compiler) optimieren oder den Compile Guard ( #pragma onceToy Compiler) nicht kompilieren können , ist er auch nicht schneller als ein Standard Include Guard.
Kevin
3
@ Kevin Aber der wichtigste Punkt ist, dass Wachen nicht fehleranfällig sind, und in 99,9% ist Pragma einmal genug, was Sie von der Sorge um dieses gute alte Wissen befreit. Im Vergleich zu Pragma einmal - sie sind wirklich kryptisch und ... langsam. ;) Langsam, nur weil du sie schreiben, pflegen und lesen musst.
Dmitry Azaraev
1
@ Kevin nach mehr als 30 Jahren mit den # ifdef-Wachen war ich froh, dass mein Kollege Kris herausgefunden hat, dass das 'neue' Pragma jetzt jetzt überall unterstützt wird. Er hat ein Skript geschrieben, um es in großen Mengen zu ersetzen, und obwohl es nur eine kleine Sache ist, ist dies die Art von Sache, die alle glücklich macht. Nochmals vielen Dank Kris!
Bert Bril
@tom kannst du ein paar Compiler nennen, die das nicht tun?
Bert Bril