Чому один раз автоматично не приймається #pragma?


81

Який сенс сказати компілятору спеціально включити файл лише один раз? Хіба це не мало б сенсу за замовчуванням? Чи існує якась причина включати один файл кілька разів? Чому б просто не припустити це? Це пов’язано з конкретним обладнанням?


24
Чи існує якась причина включати один файл кілька разів? => Може бути. У файлі може бути умовна компіляція #ifdefs. Тож можна сказати #define MODE_ONE 1і потім #include "has-modes.h", і потім #undef MODE_ONEз #define MODE_TWO 1і #include "has-modes.h"знову. Препроцесор агностично ставиться до таких видів речей, і, можливо, іноді вони можуть мати сенс.
HostileFork каже, що не довіряє SE

66
Це мало б сенс за замовчуванням. Тільки не ту, яку вони вибрали, коли програмісти С все ще їздили на конях, несли зброю та мали 16 Кб пам’яті
Ганс Пасант

11
Ви можете включити <assert.h>кілька разів, з різними визначеннями NDEBUG, в один і той же вихідний файл.
Піт Беккер,

3
Що стосується #pragma onceсамого себе, існують апаратні середовища (як правило, з мережевими дисками та можливими кількома шляхами до одного і того ж заголовка), де воно не працюватиме належним чином.
Піт Беккер,

12
Якщо ви #pragma onceприпустили, який спосіб протидії цьому замовчуванню? #pragma many? Скільки компіляторів реалізували щось подібне?
Джонатан Леффлер

Відповіді:


85

Тут є кілька пов’язаних питань:

  • Чому #pragma onceне застосовується автоматично?
    Оскільки бувають ситуації, коли потрібно включити файли більше одного разу.

  • Чому ви хочете включити файл кілька разів?
    В інших відповідях було наведено кілька причин (Boost.Preprocessor, X-Macros, включаючи файли даних). Я хотів би додати конкретний приклад "уникати дублювання коду": OpenFOAM заохочує стиль, коли введення #includeбітів та фрагментів у функції є загальним поняттям. Див., Наприклад, цю дискусію.

  • Гаразд, але чому це не типовий варіант із відмовою?
    Оскільки він фактично не визначений стандартом. #pragmas - це, за визначенням, розширення, специфічні для реалізації.

  • Чому досі #pragma onceне стало стандартизованою функцією (оскільки вона широко підтримується)?
    Тому що закріпити те, що є "одним і тим же файлом", агностично платформно, насправді напрочуд важко. Дивіться цю відповідь для отримання додаткової інформації .



4
Зокрема, див. Цей приклад для випадку, коли pragma onceне вдається, але включає охоронців, який би працював. Ідентифікація файлів за місцем розташування також не працює, оскільки іноді один і той самий файл трапляється кілька разів у проекті (наприклад, у вас є 2 підмодулі, які обидва включають у свої заголовки бібліотеку, що містить лише заголовки, і перевіряють власну копію)
MM

6
Не всі прагми є розширеннями для конкретного впровадження. Наприклад #pragma STDCсім'я . Але всі вони контролюють поведінку, визначену реалізацією.
Руслан

4
@ user4581301 Ця відповідь перебільшує проблему з прагмою один раз і не враховує проблеми через включення охоронців. В обох випадках потрібна певна дисципліна. З включення охоронців потрібно переконатися, що ви використовуєте ім'я, яке не буде використовуватися в іншому файлі (що відбудеться після зміни копії файлу). Одного разу з pragma потрібно вирішити, яке саме унікальне місце для її файлу, що, зрештою, добре.
Олів,

3
@Mehrdad: Ви серйозно пропонуєте компіляторам писати у вихідні файли !? Якщо компілятор бачить #ifndef XX, він не повинен знати, чи є щось за відповідним, #endifпоки не прочитає весь файл. Компілятор, який відслідковує, чи #ifndefзакриває весь зовнішній файл весь файл, і зазначає, який макрос, який він перевіряє, може уникнути повторного сканування файлу, але вказівка , що нічого важливого після поточного пункту, здається, здається приємнішою, ніж покладатися на компілятори для пам’ятайте такі речі.
supercat

38

Ви можете використовувати #include будь-яке місце у файлі, а не лише в глобальному масштабі, наприклад, усередині функції (і кілька разів, якщо це потрібно). Звичайно, потворний та невдалий стиль, але можливий та інколи розумний (залежно від файлу, який ви включаєте). Якби #includeколи-небудь була лише одна річ, то це не спрацювало б. #includeпросто зрештою виконує заміну німого тексту (cut'n'paste), і не все, що ви включаєте, має бути файлом заголовка. Ви можете - наприклад - #includeфайл, що містить автоматично створені дані, що містять необроблені дані, для ініціалізації файлу std::vector. Подібно до

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

І нехай "my_generated_data.txt" буде чимось, що генерується системою складання під час компіляції.

Або, можливо, я ледачий / дурний / дурний і поміщаю це у файл ( дуже надуманий приклад):

const noexcept;

а потім роблю

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

Іншим, трохи менш надуманим, прикладом може бути вихідний файл, де багатьом функціям потрібно використовувати велику кількість типів у просторі імен, але ви не хочете просто говорити using namespace foo;глобально, оскільки це призведе до того, що глобальний простір імен отримає багато іншого ти не хочеш. Отже, ви створюєте файл "foo", що містить

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

А потім ви робите це у своєму вихідному файлі

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

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

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

Примітка: Я не прихильник робити такі речі. Але це можливо і робиться в деяких кодових базах, і я не розумію, чому це не слід дозволяти. Це дійсно має свої переваги.

Також може бути, що файл, який ви включаєте, поводиться по-різному, залежно від значення певних макросів #define. Отже, ви можете захотіти включити файл у кілька розташувань, після того, як спочатку змінили якесь значення, тож ви отримаєте різну поведінку в різних частинах вихідного файлу.


1
Це все одно працювало б, якби всі заголовки були прагмою один раз. Якщо ви не включили згенеровані дані більше одного разу.
PSkocik

2
@PSkocik Але, можливо, мені потрібно це включити не раз. Чому я не маю змоги?
Jesper Juhl

2
@JesperJuhl У цьому суть. Вам не потрібно буде включати його більше одного разу. Зараз у вас є можливість, але альтернатива не набагато гірша, якщо взагалі.
Johnny Cache

9
@PSkocik Якщо я змінив значення #defines перед кожним включенням, що змінює поведінку включеного файлу, то, можливо, мені доведеться включити його кілька разів, щоб отримати різну поведінку в різних частинах мого вихідного файлу.
Jesper Juhl

27

Включення декількох разів можна, наприклад, за допомогою техніки X-macro :

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
};

Я не знаю ні про яке інше використання.


Це звучить надто складно. Будь-яка причина не просто включати ці визначення до файлу?
Джонні Кеш,

2
@JohnnyCache: Приклад - спрощена версія того, як працюють X-макроси. Будь ласка, прочитайте посилання; вони надзвичайно корисні в деяких випадках для здійснення складних маніпуляцій з табличними даними. При будь-якому значному використанні X-макросів не було б можливості просто "включити ці визначення у файл".
Нікол Болас,

2
@Johnny - так - одна вагома причина полягає у забезпеченні послідовності (це важко зробити вручну, коли у вас є лише кілька десятків елементів, неважливо сотні).
Тобі Спейт,

@TobySpeight Хе, я гадаю, я міг би пошкодувати один рядок коду, щоб не писати тисячі десь ще. Має сенс.
Johnny Cache,

1
Щоб уникнути дублювання. Особливо, якщо файл великий. Слід визнати, що ви можете просто використати великий макрос, що містить список макросів X, але оскільки проекти можуть використовувати це, встановлення вимог до #pragma onceповедінки буде надзвичайною зміною.
PSkocik

21

Здається, ви працюєте з припущенням, що метою функції "#include", навіть існуючою в мові, є надання підтримки для декомпозиції програм на кілька одиниць компіляції. Це неправильно.

Він може виконувати цю роль, але це не було його прямим призначенням. C був спочатку розроблений як мова вищого рівня, ніж PDP-11 Macro-11 Assembly для повторного впровадження Unix. Він мав макропроцесор, оскільки це була особливість Macro-11. Він мав можливість текстово включати макроси з іншого файлу, оскільки це була особливість Macro-11, якою користувався існуючий Unix, який вони переносили до свого нового компілятора C.

Тепер виявляється, що "#include" корисний для поділу коду на одиниці компіляції, як (можливо) трохи зламати. Однак той факт, що цей хакер існував, означав, що він став Шляхом, який робиться в C. Той факт, що такий спосіб існував, означав, що ніколи не потрібно було створювати новий метод для надання цієї функціональності, тому нічого безпечнішого (наприклад: не вразливий до кількох -включення) ніколи не створювався. Оскільки він уже був на мові С, його було скопійовано на С ++ разом з більшістю решти синтаксису та ідіом С.

Існує пропозиція надати C ++ належну модульну систему, щоб можна було остаточно позбутися цього 45-річного зламу препроцесора. Я не знаю, наскільки це неминуче. Я чув про це, що він працює вже більше десяти років.


5
Як зазвичай, щоб зрозуміти C та C ++, потрібно зрозуміти їх історію.
Джек Едлі

Це розумно очікувати , що модулі будуть приземлятися в лютому.
Девіс Оселедець

7
@DavisHerring - Так, але який лютий?
TED

10

Ні, це суттєво заважало б можливостям, доступним, наприклад, для бібліотечних авторів. Наприклад, Boost.Preprocessor дозволяє користуватися циклами попереднього процесора, і єдиним способом їх досягнення є багаторазове включення одного і того ж файлу.

А Boost.Preprocessor - це будівельний блок для багатьох дуже корисних бібліотек.


1
Це не буде перешкоджати будь-якому з цього. OP запитав про поведінку за замовчуванням , а не про незмінну поведінку. Було б цілком розумно змінити за замовчуванням і замість цього надати прапор препроцесора #pragma reentrantабо щось подібне. Огляд назад - 20/20.
Конрад Рудольф

Це заважало б у сенсі змусити людей оновлювати свої бібліотеки та залежності, @KonradRudolph. Не завжди проблема, але це може спричинити проблеми з деякими застарілими програмами. В ідеалі також міг би бути перемикач командного рядка, щоб вказати, за замовчуванням, onceабо reentrant, щоб пом'якшити цю чи інші потенційні проблеми.
Джастін Тайм - відновити Моніку

1
@JustinTime Ну, як мій коментар каже, що це явно не зворотно сумісна (і, отже, здійсненна) зміна. Однак питання полягало в тому, чому спочатку він був розроблений саме так, а не чому його не змінюють. І відповідь на це однозначно: оригінальний дизайн був величезною помилкою з далекосяжними наслідками.
Конрад Рудольф

8

У прошивці продукту, над яким я в основному працюю, ми повинні мати можливість вказати, де функції та глобальні / статичні змінні повинні бути розподілені в пам'яті. Обробка в режимі реального часу повинна жити в пам'яті L1 на мікросхемі, щоб процесор міг отримати швидкий доступ до неї. Менш важлива обробка може йти в пам'яті L2 на мікросхемі. І все, що не потрібно обробляти особливо оперативно, може жити у зовнішній DDR та проходити кешування, оскільки не має значення, якщо це трохи повільніше.

#Pragma, щоб виділити те, що відбувається, - це довга, нетривіальна лінія. Це було б легко помилитися. Наслідком помилки буде те, що код / ​​дані будуть мовчки поміщені в пам'ять за замовчуванням (DDR), і наслідком цього може стати контроль із замкнутим циклом, який перестане працювати без жодної причини, яку легко побачити.

Тому я використовую включені файли, які містять саме цю прагму. Мій код тепер виглядає так.

Файл заголовка ...

#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

І джерело ...

#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 */

Ви помітите там кілька включень одного і того ж файлу, навіть просто в заголовку. Якщо я щось неправильно вводжу зараз, компілятор скаже мені, що не може знайти файл включення "set_fat_storage.h", і я можу це легко виправити.

Тож у відповідь на ваше запитання це справжній практичний приклад того, де потрібно багаторазове включення.


3
Я б сказав, що ваш варіант використання є мотивуючим прикладом для _Pragmaдирективи. Ці самі прагми тепер можна розширити із звичайних макросів. Тож не потрібно включати більше одного разу.
StoryTeller - Unslander Monica
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.