Почему #pragma автоматически не считается?

Какой смысл указывать компилятору специально включать файл только один раз? Разве это не имеет смысла по умолчанию? Есть ли какая-либо причина включать один файл несколько раз? Почему бы просто не предположить это? Это связано с конкретным оборудованием?

Ответ 1

Здесь есть несколько связанных вопросов:

  • Почему #pragma once не применяется автоматически?
    Потому что есть ситуации, в которых вы хотите включить файлы более одного раза.

  • Почему вы хотите включить файл несколько раз?
    Несколько причин были приведены в других ответах (Boost.Preprocessor, X-Macros, включая файлы данных). Я хотел бы добавить конкретный пример "избегать дублирования кода": OpenFOAM поощряет стиль, в котором #include включение фрагментов в функции является общей концепцией. Смотрите, например, это обсуждение.

  • Хорошо, но почему это не по умолчанию с отказом?
    Потому что это на самом деле не указано в стандарте. #pragma по определению являются расширениями, специфичными для реализации.

  • Почему #pragma once не стала стандартизированной функцией (поскольку она широко поддерживается)?
    Потому что определить, что такое "тот же файл" не зависящим от платформы, на самом деле на удивление сложно. Смотрите этот ответ для получения дополнительной информации.

Ответ 2

Вы можете использовать #include любом месте файла, а не только в глобальном масштабе - как внутри функции (и несколько раз, если это необходимо). Конечно, уродливый и не очень хороший стиль, но возможный и иногда разумный (в зависимости от файла, который вы включаете). Если бы #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 s). Таким образом, вы можете захотеть включить файл в нескольких местах, после того как сначала изменили какое-то значение, так что вы получите различное поведение в разных частях вашего исходного файла.

Ответ 3

Можно использовать несколько раз, например, с техникой 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
};

Я не знаю ни о каком другом использовании.

Ответ 4

Похоже, вы работаете в предположении, что целью функции "#include", даже существующей в языке, является поддержка разложения программ на несколько модулей компиляции. Это неверно.

Он может выполнять эту роль, но это не было его целью. Первоначально C был разработан как язык более высокого уровня, чем сборка PDP-11 Macro-11 для переопределения Unix. У него был макропроцессор, потому что это была особенность Macro-11. Он имел возможность включать макросы из другого файла в текстовом формате, потому что это была особенность Macro-11, которую использовал существующий Unix, который они переносили на свой новый компилятор C.

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

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

Ответ 5

Нет, это существенно помешало бы опциям, доступным, например, авторам библиотек. Например, Boost.Preprocessor позволяет использовать циклы препроцессора, и единственный способ добиться этого - несколько включений одного и того же файла.

А Boost.Preprocessor является строительным блоком для многих очень полезных библиотек.

Ответ 6

В прошивке для продукта, над которым я в основном работаю, мы должны быть в состоянии указать, где функции и глобальные/статические переменные должны быть размещены в памяти. Обработка в реальном времени должна находиться в памяти 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", и я легко могу это исправить.

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