Jaki jest sens mówienia kompilatorowi, aby włączył plik tylko raz? Czy nie miałoby to domyślnie sensu? Czy jest jakiś powód, aby wielokrotnie dołączać jeden plik? Dlaczego po prostu tego nie założyć? Czy ma to związek z konkretnym sprzętem?
81
#ifdefs
. Więc możesz powiedzieć#define MODE_ONE 1
i wtedy#include "has-modes.h"
, a potem#undef MODE_ONE
z#define MODE_TWO 1
i#include "has-modes.h"
jeszcze raz. Preprocesor jest agnostykiem w stosunku do tego typu rzeczy i może czasami mają one sens.<assert.h>
wiele razy, z różnymi definicjamiNDEBUG
, w tym samym pliku źródłowym.#pragma once
siebie, istnieją środowiska sprzętowe (zwykle z dyskami sieciowymi i możliwymi wieloma ścieżkami do tego samego nagłówka), w których nie będzie działać poprawnie.#pragma once
założyłeś, jaki jest sposób przeciwdziałania temu domyślnemu?#pragma many
? Ilu kompilatorów zaimplementowało coś takiego?Odpowiedzi:
Istnieje wiele powiązanych pytań:
Dlaczego
#pragma once
nie jest egzekwowane automatycznie?Ponieważ są sytuacje, w których chcesz dołączyć pliki więcej niż raz.
Dlaczego miałbyś chcieć dołączać plik wiele razy?
W innych odpowiedziach podano kilka powodów (Boost.Preprocessor, X-Macros, w tym pliki danych). Chciałbym dodać konkretny przykład „unikania powielania kodu”: OpenFOAM zachęca do stylu, w którym umieszczanie
#include
bitów i elementów w funkcjach jest powszechną koncepcją. Zobacz na przykład tę dyskusję.Ok, ale dlaczego nie jest to opcja domyślna z opcją rezygnacji?
Ponieważ w rzeczywistości nie jest to określone w standardzie.
#pragma
s są z definicji rozszerzeniami specyficznymi dla implementacji.Dlaczego
#pragma once
nie stała się jeszcze standardową funkcją (ponieważ jest szeroko obsługiwana)?Ponieważ ustalenie tego, co jest „tym samym plikiem” w sposób niezależny od platformy, jest w rzeczywistości zaskakująco trudne. Zobacz tę odpowiedź, aby uzyskać więcej informacji .
źródło
once
pragma once
zawodzi, ale uwzględnienie strażników zadziałałoby. Identyfikowanie plików według lokalizacji również nie działa, ponieważ czasami ten sam plik występuje wiele razy w projekcie (np. Masz 2 moduły podrzędne, które zawierają bibliotekę tylko z nagłówkiem w swoich nagłówkach i pobierają własną kopię)#pragma STDC
Rodzina . Ale wszystkie kontrolują zachowanie zdefiniowane w implementacji.#ifndef XX
, nie może wiedzieć, czy coś następuje po odpowiednim,#endif
dopóki nie przeczyta całego pliku. Kompilator, który śledzi, czy najbardziej zewnętrzny#ifndef
obejmuje cały plik i zauważa, jakie makro sprawdza, może być w stanie uniknąć ponownego skanowania pliku, ale dyrektywa mówiąca, że nie ma nic ważnego po bieżącym punkcie, wydawałaby się ładniejsza niż poleganie na kompilatorach pamiętaj o takich rzeczach.Możesz używać w
#include
dowolnym miejscu w pliku, nie tylko w zakresie globalnym - jak np. Wewnątrz funkcji (i wielokrotnie, jeśli to konieczne). Jasne, brzydkie i nie w dobrym stylu, ale możliwe i czasami rozsądne (w zależności od dołączonego pliku). Gdyby to#include
było tylko jednorazowe, to by nie zadziałało.#include
po prostu robi głupie podstawianie tekstu (wytnij i wklej) w końcu, a nie wszystko, co dołączasz, musi być plikiem nagłówkowym. Możesz - na przykład -#include
plik zawierający automatycznie wygenerowane dane zawierające surowe dane do zainicjowania plikustd::vector
. Lubićstd::vector<int> data = { #include "my_generated_data.txt" }
I niech „my_generated_data.txt” będzie czymś wygenerowanym przez system kompilacji podczas kompilacji.
A może jestem leniwy / głupi / głupi i umieściłem to w pliku ( bardzo wymyślny przykład):
const noexcept;
a potem robię
class foo { void f1() #include "stupid.file" int f2(int) #include "stupid.file" };
Innym, nieco mniej wymyślnym przykładem byłby plik źródłowy, w którym wiele funkcji musi używać dużej liczby typów w przestrzeni nazw, ale nie chcesz mówić tylko
using namespace foo;
globalnie, ponieważ spowodowałoby to polutowanie globalnej przestrzeni nazw wieloma innymi rzeczami nie chcesz. Więc tworzysz plik „foo” zawierającyusing std::vector; using std::array; using std::rotate; ... You get the idea ...
A potem robisz to w swoim pliku źródłowym
void f1() { #include "foo" // needs "stuff" } void f2() { // Doesn't need "stuff" } void f3() { #include "foo" // also needs "stuff" }
Uwaga: nie zalecam robienia takich rzeczy. Ale jest to możliwe i zrobione w niektórych bazach kodu i nie rozumiem, dlaczego nie powinno to być dozwolone. To ma mieć swoje zastosowanie.
Może się również zdarzyć, że dołączony plik zachowuje się inaczej w zależności od wartości niektórych makr
#define
. Możesz więc chcieć dołączyć plik w wielu lokalizacjach, po uprzedniej zmianie pewnej wartości, aby uzyskać różne zachowanie w różnych częściach pliku źródłowego.źródło
#define
s przed każdym dołączeniem, które zmienia zachowanie dołączonego pliku, być może będę musiał dołączyć go wiele razy, aby uzyskać te różne zachowania w różnych częściach mojego pliku źródłowego.Uwzględnianie wielokrotności jest użyteczne np. W przypadku techniki X-makro :
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 };
Nie wiem o żadnym innym zastosowaniu.
źródło
#pragma once
zachowanie mandatu byłoby przełomową zmianą.Wydaje się, że działasz przy założeniu, że celem funkcji „#include”, nawet istniejącej w języku, jest zapewnienie wsparcia dla dekompozycji programów na wiele jednostek kompilacji. To jest niepoprawne.
Może pełnić tę rolę, ale nie taki był jego cel. C został pierwotnie opracowany jako język nieco wyższego poziomu niż zestaw PDP-11 Macro-11 w celu ponownej implementacji Uniksa. Miał preprocesor makr, ponieważ była to cecha Macro-11. Miał możliwość tekstowego dołączania makr z innego pliku, ponieważ była to cecha Macro-11, z której korzystał istniejący Unix, który przenosili na swój nowy kompilator C.
Teraz okazuje się, że „#include” przydaje się do rozdzielania kodu na jednostki kompilacyjne, jako (prawdopodobnie) odrobina hackowania. Jednak fakt, że ten hack istniał, oznaczał, że stał się Drogą wykonywaną w C. Fakt, że taki sposób istniał, oznaczał, że żadna nowa metoda nigdy nie była potrzebna było tworzyć aby zapewnić tę funkcjonalność, więc nic bezpieczniejszego (np. -inclusion) został kiedykolwiek stworzony. Ponieważ był już w C, został skopiowany do C ++ wraz z większością reszty składni i idiomów C.
Jest propozycja, aby dać C ++ odpowiedni system modułów, aby można było wreszcie zrezygnować z tego 45-letniego hackowania preprocesorów. Nie wiem jednak, jakie to nieuchronne. Słyszałem o tym, że pracuje od ponad dekady.
źródło
Nie, to znacznie utrudniłoby opcje dostępne, na przykład, autorom bibliotek. Na przykład Boost.Preprocessor pozwala na użycie pętli preprocesora, a jedynym sposobem na osiągnięcie tego jest wielokrotne włączanie tego samego pliku.
Boost.Preprocessor to element składowy wielu bardzo przydatnych bibliotek.
źródło
#pragma reentrant
lub czegoś podobnego. Z perspektywy czasu jest 20/20.once
czyreentrant
, aby złagodzić ten lub inne potencjalne problemy.W oprogramowaniu sprzętowym produktu, nad którym głównie pracuję, musimy być w stanie określić, gdzie w pamięci mają być alokowane funkcje i zmienne globalne / statyczne. Przetwarzanie w czasie rzeczywistym musi znajdować się w pamięci L1 na chipie, aby procesor mógł uzyskać do niego bezpośredni i szybki dostęp. Mniej ważne przetwarzanie może znajdować się w pamięci L2 na chipie. A wszystko, co nie wymaga szczególnie szybkiej obsługi, może żyć w zewnętrznym DDR i przechodzić przez buforowanie, ponieważ nie ma znaczenia, czy jest trochę wolniejsze.
#Pragma, która ma zostać przydzielona, to długa, nietrywialna linia. Łatwo byłoby się pomylić. Efektem błędnego wykonania byłoby to, że kod / dane zostałyby po cichu umieszczone w pamięci domyślnej (DDR), a skutkiem tego mogłoby być zatrzymanie działania sterowania w zamkniętej pętli bez łatwo zauważalnego powodu.
Więc używam plików dołączanych, które zawierają tylko tę pragmę. Mój kod wygląda teraz tak.
Plik nagłówkowy ...
#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
I źródło ...
#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 */
Zauważysz tam wiele inkluzji tego samego pliku, nawet tylko w nagłówku. Jeśli teraz coś pomylę, kompilator powie mi, że nie może znaleźć pliku dołączanego „set_fat_storage.h” i mogę to łatwo naprawić.
Odpowiadając na twoje pytanie, jest to prawdziwy, praktyczny przykład tego, gdzie wymagane jest wielokrotne włączanie.
źródło
_Pragma
dyrektywy. Te same pragmy można teraz rozwinąć ze zwykłych makr. Nie ma więc potrzeby uwzględniania więcej niż raz.