Warum wird #pragma nicht einmal automatisch angenommen?

81

Was bringt es dem Compiler, die Datei nur einmal einzuschließen? Wäre es nicht standardmäßig sinnvoll? Gibt es überhaupt einen Grund, eine einzelne Datei mehrmals einzuschließen? Warum nicht einfach davon ausgehen? Hat es mit spezifischer Hardware zu tun?

Johnny Cache
quelle
24
Gibt es überhaupt einen Grund, eine einzelne Datei mehrmals einzuschließen? => Könnte sein. Eine Datei enthält möglicherweise eine bedingte Kompilierung #ifdefs. Also könnte man sagen #define MODE_ONE 1und dann #include "has-modes.h"und dann #undef MODE_ONEmit #define MODE_TWO 1und #include "has-modes.h"wieder. Der Präprozessor ist in Bezug auf diese Art von Dingen agnostisch, und vielleicht können sie manchmal Sinn machen.
HostileFork sagt, vertraue SE
66
Dies wäre standardmäßig sinnvoll. Nur nicht die, die sie ausgewählt hatten, als C-Programmierer noch Pferde ritten, Waffen trugen und
Hans Passant
11
Sie können <assert.h>mehrere Male mit unterschiedlichen Definitionen NDEBUGin dieselbe Quelldatei aufnehmen.
Pete Becker
3
Was #pragma onceselbst gibt es Hardware - Umgebungen ( in der Regel mit vernetzten Antrieben und können mehrere Pfade zu demselben Header) , wo es nicht richtig funktioniert.
Pete Becker
12
Wenn Sie #pragma onceangenommen haben, wie können Sie diesem Standard widersprechen? #pragma many? Wie viele Compiler haben so etwas implementiert?
Jonathan Leffler

Antworten:

84

Hier gibt es mehrere verwandte Fragen:

  • Warum wird #pragma oncenicht automatisch erzwungen?
    Weil es Situationen gibt, in denen Sie Dateien mehrmals einschließen möchten.

  • Warum sollten Sie eine Datei mehrmals einfügen?
    In anderen Antworten wurden mehrere Gründe angegeben (Boost.Preprocessor, X-Macros, einschließlich Datendateien). Ich möchte ein besonderes Beispiel für "Vermeiden von Codeduplizierungen" hinzufügen: OpenFOAM fördert einen Stil, bei dem #includees ein gängiges Konzept ist, Teile innerhalb von Funktionen zu verwenden. Siehe zum Beispiel diese Diskussion.

  • Ok, aber warum ist es nicht die Standardeinstellung mit einem Opt-out?
    Weil es nicht durch den Standard spezifiziert ist. #pragmas sind per Definition implementierungsspezifische Erweiterungen.

  • Warum ist es noch #pragma oncekein standardisiertes Feature geworden (da es weitgehend unterstützt wird)?
    Weil es überraschend schwierig ist, die "gleiche Datei" plattformunabhängig festzuhalten. Weitere Informationen finden Sie in dieser Antwort .

Max Langhof
quelle
Und eine andereonce
Einstellung
4
In diesem Beispiel finden Sie insbesondere einen Fall, in dem pragma onceFehler aufgetreten sind, aber Wachen eingeschlossen wären. Das Identifizieren von Dateien nach Speicherort funktioniert auch nicht, da dieselbe Datei in einem Projekt manchmal mehrmals vorkommt (z. B. haben Sie zwei Submodule, die beide eine Nur-Header-Bibliothek in ihren Headern enthalten und ihre eigene Kopie davon
MM
6
Nicht alle Pragmas sind implementierungsspezifische Erweiterungen. ZB #pragma STDCFamilie . Sie alle steuern jedoch das implementierungsdefinierte Verhalten.
Ruslan
3
@ user4581301 Diese Antwort übertreibt das Problem mit Pragma einmal und berücksichtigt keine Probleme aufgrund von Wachen. In beiden Fällen ist etwas Disziplin erforderlich. Bei Include Guards muss sichergestellt werden, dass ein Name verwendet wird, der nicht in einer anderen Datei verwendet wird (was nach einer Änderung der Dateikopie geschieht). Bei Pragma muss man sich einmal entscheiden, was der einzigartige richtige Ort für seine Datei ist, was schließlich eine gute Sache ist.
Oliv
3
@Mehrdad: Schlagen Sie ernsthaft vor, dass Compiler in Quelldateien schreiben? Wenn ein Compiler sieht #ifndef XX, muss er nicht wissen, ob nach dem Entsprechenden etwas folgt, #endifbis er die gesamte Datei gelesen hat. Ein Compiler, der #ifndefnachverfolgt, ob der äußerste die gesamte Datei einschließt und feststellt, welches Makro er überprüft, kann möglicherweise ein erneutes Scannen der Datei vermeiden. Eine Anweisung, zu sagen, dass nach dem aktuellen Punkt nichts von Bedeutung ist, scheint jedoch sinnvoller zu sein, als sich auf Compiler zu verlassen erinnere dich an solche Dinge.
Supercat
38

Sie können #include überall in einer Datei verwenden, nicht nur im globalen Bereich - wie innerhalb einer Funktion (und bei Bedarf mehrmals). Sicher, hässlich und nicht gut, aber möglich und gelegentlich sinnvoll (abhängig von der Datei, die Sie einschließen). Wenn #includees nur eine einmalige Sache wäre, würde das nicht funktionieren. #includemacht doch nur dumme Textersetzung (cut'n'paste), und nicht alles, was Sie einschließen, muss eine Header-Datei sein. Sie können beispielsweise #includeeine Datei mit automatisch generierten Daten enthalten, die die Rohdaten zum Initialisieren von a enthalten std::vector. Mögen

std::vector<int> data = {
#include "my_generated_data.txt"
}

Und muss "my_generated_data.txt" etwas sein, das vom Build-System während der Kompilierung generiert wird.

Oder vielleicht bin ich faul / albern / dumm und füge dies in eine Datei ein ( sehr erfundenes Beispiel):

const noexcept;

und dann tue ich es

class foo {
    void f1()
    #include "stupid.file"
    int f2(int)
    #include "stupid.file"
};

Ein anderes, etwas weniger ausgeklügeltes Beispiel wäre eine Quelldatei, in der viele Funktionen eine große Anzahl von Typen in einem Namespace verwenden müssen, aber Sie möchten nicht nur using namespace foo;global sagen, da dies den globalen Namespace mit vielen anderen Dingen verschmutzen würde du willst nicht Sie erstellen also eine Datei "foo" mit

using std::vector;
using std::array;
using std::rotate;
... You get the idea ...

Und dann tun Sie dies in Ihrer Quelldatei

void f1() {
#include "foo" // needs "stuff"
}

void f2() {
    // Doesn't need "stuff"
}

void f3() {
#include "foo" // also needs "stuff"
}

Hinweis: Ich befürworte nicht, solche Dinge zu tun. Aber es ist möglich und in einigen Codebasen erledigt und ich verstehe nicht, warum es nicht erlaubt sein sollte. Es hat seine Verwendung.

Es kann auch sein, dass sich die von Ihnen eingeschlossene Datei je nach Wert bestimmter Makros unterschiedlich verhält #define. Daher möchten Sie die Datei möglicherweise an mehreren Speicherorten einfügen, nachdem Sie zuerst einen Wert geändert haben, damit Sie in verschiedenen Teilen Ihrer Quelldatei ein unterschiedliches Verhalten erhalten.

Jesper Juhl
quelle
1
Dies würde immer noch funktionieren, wenn alle Header einmal Pragma wären. Solange Sie die generierten Daten nicht mehr als einmal aufgenommen haben.
PSkocik
2
@PSkocik Aber vielleicht ich brauche , um es mehr zu umfassen als einmal. Warum sollte ich nicht dazu in der Lage sein?
Jesper Juhl
2
@JesperJuhl Das ist der Punkt. Sie müssen es nicht mehr als einmal einfügen. Sie haben derzeit die Option, aber die Alternative ist nicht viel schlimmer, wenn überhaupt.
Johnny Cache
9
@PSkocik Wenn ich den Wert von #defines vor jedem Include ändere, der das Verhalten der enthaltenen Datei ändert, muss ich ihn möglicherweise mehrmals einschließen, um diese unterschiedlichen Verhaltensweisen in verschiedenen Teilen meiner Quelldatei zu erhalten.
Jesper Juhl
27

Das mehrfache Einschließen ist zB mit der X-Makro- Technik möglich:

data.inc:

X(ONE)
X(TWO)
X(THREE)

use_data_inc_twice.c

enum data_e { 
#define X(V) V,
   #include "data.inc"
#undef X
};
char const* data_e__strings[]={
#define X(V) [V]=#V,
   #include "data.inc"
#undef X
};

Ich weiß nichts über eine andere Verwendung.

PSkocik
quelle
Das klingt zu komplex. Gibt es einen Grund, diese Definitionen nicht erst in die Datei aufzunehmen?
Johnny Cache
2
@JohnnyCache: Das Beispiel ist eine vereinfachte Version der Funktionsweise von X-Makros. Bitte lesen Sie den Link; Sie sind in einigen Fällen äußerst nützlich für komplexe Manipulationen von Tabellendaten. Bei einer signifikanten Verwendung von X-Makros gibt es keine Möglichkeit, diese Definitionen einfach in die Datei aufzunehmen.
Nicol Bolas
2
@ Johnny - ja - ein guter Grund ist die Gewährleistung der Konsistenz (schwer von Hand zu machen, wenn Sie nur ein paar Dutzend Elemente haben, egal Hunderte).
Toby Speight
@TobySpeight Heh, ich denke, ich könnte eine einzelne Codezeile sparen, um zu vermeiden, dass Tausende woanders geschrieben werden. Macht Sinn.
Johnny Cache
1
Um Doppelarbeit zu vermeiden. Besonders wenn die Datei groß ist. Zugegeben, Sie könnten einfach ein großes Makro verwenden, das die X-Makroliste enthält, aber da Projekte dies verwenden könnten, #pragma oncewäre das Mandatieren von Verhalten eine bahnbrechende Änderung.
PSkocik
21

Sie scheinen unter der Annahme zu arbeiten, dass der Zweck der Funktion "#include", die sogar in der Sprache vorhanden ist, darin besteht, die Zerlegung von Programmen in mehrere Kompilierungseinheiten zu unterstützen. Das ist falsch.

Es kann diese Rolle übernehmen, aber das war nicht der beabsichtigte Zweck. C wurde ursprünglich als etwas höhere Sprache als PDP-11 Macro-11 Assembly für die Neuimplementierung von Unix entwickelt. Es hatte einen Makro-Präprozessor, da dies eine Funktion von Macro-11 war. Es war in der Lage, Makros aus einer anderen Datei in Textform einzuschließen, da dies eine Funktion von Macro-11 war, die das vorhandene Unix, das sie auf ihren neuen C-Compiler portierten, verwendet hatte.

Nun stellt sich heraus, dass "#include" nützlich ist , um Code in Kompilierungseinheiten zu unterteilen, als (wohl) ein bisschen wie ein Hack. Die Tatsache jedoch, dass dieser Hack bestanden bedeutet , dass es wurde der Weg, der in C. Die Tatsache erfolgt , dass ein Weg , kein neues Verfahren jemals existierte gemeint benötigt geschaffen werden , um diese Funktionalität bereitzustellen, so dass nichts sicherer (zB: nicht anfällig für mehr -einschluss) wurde jemals erstellt. Da es bereits in C war, wurde es zusammen mit den meisten anderen Syntaxen und Redewendungen von C in C ++ kopiert.

Es gibt einen Vorschlag, C ++ ein geeignetes Modulsystem zu geben, damit auf diesen 45 Jahre alten Präprozessor-Hack endlich verzichtet werden kann. Ich weiß allerdings nicht, wie unmittelbar dies ist. Ich habe seit mehr als einem Jahrzehnt davon gehört, dass es in Arbeit ist.

TED
quelle
5
Wie üblich müssen Sie zum Verständnis von C und C ++ deren Verlauf verstehen.
Jack Aidley
Es ist zu erwarten, dass die Module im Februar landen werden.
Davis Herring
7
@DavisHerring - Ja, aber welcher Februar?
TED
10

Nein, dies würde die Optionen, die beispielsweise Bibliotheksautoren zur Verfügung stehen, erheblich beeinträchtigen. Mit Boost.Preprocessor können beispielsweise Vorprozessorschleifen verwendet werden. Diese können nur durch mehrere Einschlüsse derselben Datei erreicht werden.

Und Boost.Preprocessor ist ein Baustein für viele sehr nützliche Bibliotheken.

SergeyA
quelle
1
Es wäre nicht behindern jede davon. OP fragte nach einem Standardverhalten , nicht nach einem unveränderlichen Verhalten. Es wäre durchaus sinnvoll, die Standardeinstellung zu ändern und stattdessen ein Präprozessor-Flag #pragma reentrantoder ähnliches bereitzustellen . Rückblick ist 20/20.
Konrad Rudolph
Dies würde es in dem Sinne behindern, dass die Leute gezwungen werden, ihre Bibliotheken und Abhängigkeiten zu aktualisieren, @KonradRudolph. Nicht immer ein Problem, aber es kann Probleme mit einigen Legacy-Programmen verursachen. Im Idealfall gibt es auch einen Befehlszeilenschalter, mit dem angegeben werden kann, ob der Standardwert onceoder reentrantist, um dieses oder andere potenzielle Probleme zu verringern.
Justin Time - Stellen Sie Monica
1
@JustinTime Wie mein Kommentar sagt, handelt es sich eindeutig nicht um eine abwärtskompatible (und daher machbare) Änderung. Die Frage war jedoch, warum es ursprünglich so entworfen wurde und nicht, warum es nicht geändert wird. Und die Antwort darauf ist eindeutig, dass das ursprüngliche Design ein großer Fehler mit weitreichenden Konsequenzen war.
Konrad Rudolph
8

In der Firmware für das Produkt, an dem ich hauptsächlich arbeite, müssen wir angeben können, wo Funktionen und globale / statische Variablen im Speicher zugewiesen werden sollen. Die Echtzeitverarbeitung muss im L1-Speicher auf dem Chip gespeichert sein, damit der Prozessor direkt und schnell darauf zugreifen kann. Eine weniger wichtige Verarbeitung kann im L2-Speicher auf dem Chip erfolgen. Und alles, was nicht besonders schnell erledigt werden muss, kann in der externen DDR leben und das Caching durchlaufen, da es keine Rolle spielt, ob es etwas langsamer ist.

Das # Pragma, um zuzuweisen, wohin die Dinge gehen, ist eine lange, nicht triviale Linie. Es wäre leicht, etwas falsch zu machen. Die Wirkung der es falsch bekommen wäre, dass die Code / Daten würden lautlos in Standard (DDR) Speicher abgelegt werden, und die Wirkung , dass Closed-Loop werden könnten Steuer Anhalten ohne Grund arbeiten , die leicht zu sehen ist.

Also benutze ich Include-Dateien, die genau dieses Pragma enthalten. Mein Code sieht jetzt so aus.

Header-Datei...

#ifndef HEADERFILE_H
#define HEADERFILE_H

#include "set_fast_storage.h"

/* Declare variables */

#include "set_slow_storage.h"

/* Declare functions for initialisation on startup */

#include "set_fast_storage.h"

/* Declare functions for real-time processing */

#include "set_storage_default.h"

#endif

Und Quelle ...

#include "headerfile.h"

#include "set_fast_storage.h"

/* Define variables */

#include "set_slow_storage.h"

/* Define functions for initialisation on startup */

#include "set_fast_storage.h"

/* Define functions for real-time processing */

Sie werden dort mehrere Einschlüsse derselben Datei bemerken, sogar nur in der Kopfzeile. Wenn ich jetzt etwas falsch schreibe, teilt mir der Compiler mit, dass die Include-Datei "set_fat_storage.h" nicht gefunden werden kann, und ich kann das Problem leicht beheben.

Als Antwort auf Ihre Frage ist dies ein reales, praktisches Beispiel dafür, wo eine mehrfache Einbeziehung erforderlich ist.

Graham
quelle
3
Ich würde sagen, Ihr Anwendungsfall ist ein motivierendes Beispiel für die _PragmaRichtlinie. Dieselben Pragmas können jetzt aus regulären Makros erweitert werden. Es muss also nicht mehr als einmal eingeschlossen werden.
Geschichtenerzähler - Unslander Monica