Dlaczego #pragma nie jest raz przyjęta automatycznie?

81

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?

Johnny Cache
źródło
24
Czy jest jakiś powód, aby wielokrotnie dołączać jeden plik? => Może. Plik może zawierać kompilację warunkową #ifdefs. Więc możesz powiedzieć #define MODE_ONE 1i wtedy #include "has-modes.h", a potem #undef MODE_ONEz #define MODE_TWO 1i #include "has-modes.h"jeszcze raz. Preprocesor jest agnostykiem w stosunku do tego typu rzeczy i może czasami mają one sens.
HostileFork mówi, że nie ufaj SE
66
Miałoby to sens jako domyślne. Po prostu nie ten, który wybrali, gdy programiści C nadal jeździli konno, nosili broń i mieli 16 KB pamięci
Hans Passant
11
Możesz dołączyć <assert.h>wiele razy, z różnymi definicjami NDEBUG, w tym samym pliku źródłowym.
Pete Becker
3
Jeśli chodzi o #pragma oncesiebie, 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.
Pete Becker
12
Jeśli #pragma oncezałożyłeś, jaki jest sposób przeciwdziałania temu domyślnemu? #pragma many? Ilu kompilatorów zaimplementowało coś takiego?
Jonathan Leffler

Odpowiedzi:

85

Istnieje wiele powiązanych pytań:

  • Dlaczego #pragma oncenie 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 #includebitów i elementów w funkcjach jest powszechną koncepcją. Zobacz na przykład dyskusję.

  • Ok, ale dlaczego nie jest to opcja domyślna z opcją rezygnacji?
    Ponieważ w rzeczywistości nie jest to określone w standardzie. #pragmas są z definicji rozszerzeniami specyficznymi dla implementacji.

  • Dlaczego #pragma oncenie 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 .

Max Langhof
źródło
4
W szczególności zobacz ten przykład, aby zapoznać się z przypadkiem, w którym pragma oncezawodzi, 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ę)
MM
6
Nie wszystkie pragmy są rozszerzeniami specyficznymi dla implementacji. Np . #pragma STDCRodzina . Ale wszystkie kontrolują zachowanie zdefiniowane w implementacji.
Rusłan
4
@ user4581301 Ta odpowiedź przesadnie wyolbrzymia problem z pragmą raz i nie uwzględnia problemów związanych z włączeniem strażników. W obu przypadkach potrzebna jest dyscyplina. Z włączonymi strażnikami należy upewnić się, że użyjesz nazwy, która nie będzie używana w innym pliku (co nastąpi po modyfikacji kopii pliku). Razem z pragmą trzeba zdecydować, jakie jest wyjątkowe miejsce na jej plik, co w końcu jest dobrą rzeczą.
Oliv
3
@Mehrdad: Czy poważnie sugerujesz, że kompilatory zapisują pliki źródłowe !? Jeśli kompilator widzi #ifndef XX, nie może wiedzieć, czy coś następuje po odpowiednim, #endifdopóki nie przeczyta całego pliku. Kompilator, który śledzi, czy najbardziej zewnętrzny #ifndefobejmuje 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.
supercat
38

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 #includebyło tylko jednorazowe, to by nie zadziałało. #includepo 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 - #includeplik zawierający automatycznie wygenerowane dane zawierające surowe dane do zainicjowania pliku std::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ący

using 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.

Jesper Juhl
źródło
1
To by nadal działało, gdyby wszystkie nagłówki były raz pragmą. O ile nie podałeś wygenerowanych danych więcej niż raz.
PSkocik
2
@PSkocik Ale może muszę to dodać więcej niż raz. Dlaczego nie miałbym być w stanie?
Jesper Juhl
2
@JesperJuhl O to chodzi. Nie będziesz musiał uwzględniać go więcej niż raz. Obecnie masz taką opcję, ale alternatywa nie jest dużo gorsza, jeśli w ogóle.
Johnny Cache
9
@PSkocik Jeśli zmienię wartość #defines 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.
Jesper Juhl
27

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.

PSkocik
źródło
To brzmi zbyt skomplikowanie. Czy jest jakiś powód, aby nie umieszczać tych definicji w pliku w pierwszej kolejności?
Johnny Cache
2
@JohnnyCache: Przykład jest uproszczoną wersją działania X-makr. Przeczytaj link; są niezwykle przydatne w niektórych przypadkach do wykonywania złożonych operacji na danych tabelarycznych. Przy jakimkolwiek znaczącym użyciu X-macros nie byłoby sposobu, aby po prostu „dołączyć te definicje do pliku”.
Nicol Bolas
2
@Johnny - tak - jednym dobrym powodem jest zapewnienie spójności (trudne do zrobienia ręcznie, gdy masz zaledwie kilkadziesiąt elementów, nie mówiąc już o setkach).
Toby Speight
@TobySpeight Heh, przypuszczam, że mógłbym poświęcić jedną linię kodu, aby uniknąć pisania tysięcy w innym miejscu. Ma sens.
Johnny Cache
1
Aby uniknąć powielania. Zwłaszcza jeśli plik jest duży. Wprawdzie możesz po prostu użyć dużego makra zawierającego listę makr X, ale ponieważ projekty mogą tego używać, #pragma oncezachowanie mandatu byłoby przełomową zmianą.
PSkocik
21

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.

PRZETRZĄSAĆ
źródło
5
Jak zwykle, aby zrozumieć C i C ++, musisz zrozumieć ich historię.
Jack Aidley
Można się spodziewać, że moduły wylądują w lutym.
Davis Herring
7
@DavisHerring - Tak, ale w którym lutym?
TED
10

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.

Siergiej A.
źródło
1
To nie przeszkadza każdy o tym. OP zapytał o zachowanie domyślne , a nie niezmienne. Całkowicie rozsądna byłaby zmiana wartości domyślnej i zamiast tego zapewnienie flagi preprocesora #pragma reentrantlub czegoś podobnego. Z perspektywy czasu jest 20/20.
Konrad Rudolph
Utrudniłoby to w sensie zmuszania ludzi do aktualizowania ich bibliotek i zależności, @KonradRudolph. Nie zawsze jest to problem, ale może powodować problemy z niektórymi starszymi programami. W idealnym przypadku byłby również przełącznik wiersza polecenia, aby określić, czy wartość domyślna to, onceczy reentrant, aby złagodzić ten lub inne potencjalne problemy.
Justin Time - Przywróć Monikę
1
@JustinTime Cóż, jak mówi mój komentarz, wyraźnie nie jest to zmiana kompatybilna wstecz (a zatem wykonalna). Pytanie jednak brzmiało, dlaczego początkowo został zaprojektowany w ten sposób, a nie dlaczego nie jest zmieniany. A odpowiedź brzmi jednoznacznie, że oryginalny projekt był wielkim błędem o daleko idących konsekwencjach.
Konrad Rudolph
8

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.

Graham
źródło
3
Powiedziałbym, że pański przypadek użycia jest motywującym przykładem dla _Pragmadyrektywy. Te same pragmy można teraz rozwinąć ze zwykłych makr. Nie ma więc potrzeby uwzględniania więcej niż raz.
StoryTeller - Unslander Monica