Почему C/С++ "#pragma once" не соответствует стандарту ISO?

В настоящее время я работаю над большим проектом и поддерживаю все, что включает охранников, что делает меня сумасшедшим! Написание его вручную - это пустая трата времени. Хотя многие редакторы могут создавать включенные охранники, это не очень помогает:

  • Редактор генерирует защитный символ на основе имени файла. Проблема возникает, когда у вас есть заголовки с одинаковым именем файла в разных каталогах. Оба они получат то же самое, что и охранник. Включение структуры каталога в символ защиты потребует от редактора особого подхода, поскольку слэши и обратные слэши в макросе не самые лучшие.

  • Когда мне нужно переименовать файл, я должен также переименовать все защитные элементы include (в комментарии ifndef, define и ideal endif). Раздражает.

  • Препроцессор заливается множеством символов без подсказки, что они означают.

  • Тем не менее определение включается один раз, компилятор все равно открывает заголовок каждый раз, когда он встречает включение заголовка.

  • Включить защитные устройства не вписываются в пространства имен или шаблоны. На самом деле они подрывают пространства имен!

  • У вас есть шанс, что ваш символ защиты не будет уникальным.

Возможно, они были приемлемым решением в тех случаях, когда программы содержали менее 1000 заголовков в одном каталоге. Но сейчас? Он древний, он не имеет ничего общего с современными привычками кодирования. Что беспокоит меня больше всего, так это то, что эти вопросы могут быть практически полностью решены директивой #pragma once. Почему это не стандарт?

Ответ 1

Директива типа #pragma once не является тривиальной для определения полностью переносимым способом, который имеет четкие однозначные преимущества. Некоторые из концепций, для которых возникают вопросы, недостаточно четко определены для всех систем, поддерживающих C, и определение его простым способом может не принести никакой пользы по сравнению с обычными защитными устройствами.

Когда компиляция встречает #pragma once, как он должен идентифицировать этот файл, чтобы он не включал его содержимое снова?

Очевидным ответом является уникальное расположение файла в системе. Это прекрасно, если система имеет уникальные местоположения для всех файлов, но многие системы предоставляют ссылки (символические ссылки и жесткие ссылки), которые означают, что "файл" не имеет уникального местоположения. Должен ли файл быть повторно включен только потому, что он был найден с помощью другого имени? Наверное, нет.

Но теперь возникает проблема, как можно определить поведение #pragma once таким образом, чтобы иметь точный смысл на всех платформах - даже те, у которых даже нет каталогов, не говоря уже о символических ссылках, и все еще получить желаемое поведение в системах, которые имеют их?

Можно сказать, что идентификатор файлов определяется его содержимым, поэтому, если включенный файл имеет #pragma once, и файл включен, который имеет точно такое же содержимое, то второй и последующий #include не будут иметь эффекта.

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

В обратном порядке каждый раз, когда встречный файл встречается с #pragma once, его содержимое должно быть проверено на каждый другой файл с помощью #pragma once, который уже был включен до сих пор. Это подразумевает удар производительности, аналогичный использованию защитных устройств #include в любом случае, и добавляет несущественную нагрузку для авторов компилятора. Очевидно, что результаты этого могут быть кэшированы, но то же самое верно для обычных включенных охранников.

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

Учитывая потенциальные ловушки и издержки, а также тот факт, что обычные включают охранников, мне неудивительно, что комитет по стандартам не чувствовал необходимости стандартизировать #pragma once.

Ответ 2

Включение охранников, безусловно, является досадой, и C должен был первоначально быть спроектирован таким образом, чтобы заголовки включались один раз по умолчанию - требуя, чтобы некоторые специальные опции включали заголовок несколько раз.

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

Лично я решаю вашу 1-ю проблему (похожие имена включают файлы), добавляя GUID к включенному защитнику. Это уродливо, и большинство людей его ненавидят (поэтому я часто вынужден не использовать его на работе), но ваш опыт показывает, что идея имеет какую-то ценность - даже если она ужасно уродлива (но опять же все это включает в себя охрану вид взлома - почему бы не отправиться на целый боров?):

#ifndef C_ASSERT_H_3803b949_b422_4377_8713_ce606f29d546
#define C_ASSERT_H_3803b949_b422_4377_8713_ce606f29d546

// blah blah blah...

#endif

Я слышал, что компиляторы фактически не открывают заголовочные файлы, которые включают в себя охранники (они научились распознавать идиому). Я не уверен, что это правда (или в какой степени это правда); Я никогда не измерял это. Я также не беспокоюсь об этом, но мои проекты не настолько велики, что это проблема.

Мой GUID-хак в значительной степени решает пункты 1, 5 и 6. Я просто живу с элементами 2, 3 и 4. На самом деле, для пункта 2 вы можете жить без переименования включенного макроса-хранителя при переименовании файла в качестве GUID обеспечит его уникальность. На самом деле, нет никаких оснований включать имя файла вообще с GUID. Но я - традиция, я полагаю.

Ответ 3

  • Как часто вам приходится добавлять в этот проект файл include? Неужели так сложно добавить DIRNAME_FILENAME к охраннику? И всегда есть GUID.
  • Вы действительно часто переименовываете файлы? Когда-либо? Кроме того, включение GUARD в #endif так же раздражает, как и любой другой бесполезный комментарий.
  • Я сомневаюсь, что ваш защитный файл 1000 заголовков определяет даже небольшой процент от числа определений, генерируемых вашими системными библиотеками (особенно в Windows).
  • Я думаю, что MSC 10 для DOS (20+ лет назад) отслеживал, какие заголовки были включены, и если они содержат защитников, они пропустили бы их, если они будут включены снова. Это старая технология.
  • пространства имен и шаблоны не должны охватывать заголовки. Дорогой, не говори, что ты это делаешь:

    template <typename foo>
    class bar {
    #include "bar_impl.h"
    };
    
  • Вы уже это сказали.

Ответ 4

Как уже отмечалось, в С++ Standard должны учитываться разные платформы разработки, некоторые из которых могут иметь ограничения, позволяющие реализовать #pragma после поддержки.

С другой стороны, поддержка потоков не была добавлена ​​по той же причине ранее, но более новый стандарт С++ включает потоки тем не менее. И в последнем случае мы можем перекрестно скомпилировать для очень ограниченной платформы, но разработка осуществляется на полноценной платформе. Поскольку GCC поддерживает это расширение, я думаю, реальный ответ на ваш вопрос заключается в том, что нет заинтересованной стороны в продвижении этой функции в С++ Standard.

С практической точки зрения, включение охранников вызвало у нашей команды больше проблем, чем несоответствие директивы #pragma однажды. Например, GUID в include guard не помогает в случае, если файл дублируется, и позже оба экземпляра включены. При использовании только #pragma, как только мы получим повторяющуюся ошибку определения и можем потратить время на объединение исходного кода. Но в случае включения охранников проблема может потребовать проверки во время выполнения, например. это происходит, если копии отличаются аргументами по умолчанию для параметров функции.

Избегайте использования охранников. Если мне нужно перенести мой код в компилятор без поддержки #pragma после этого, я напишу script, который добавит include guard для всех файлов заголовков.

Ответ 5

Проблема возникает, когда у вас есть заголовки с одинаковым именем файла в разных каталогах.

Итак, у вас есть два заголовка, которые оба называются ice_cream_maker.h в вашем проекте, оба из которых имеют класс с именем ice_cream_maker, определенный в них, который выполняет ту же функцию? Или вы вызываете каждый класс в вашей системе foo?

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

Отредактируйте код, чтобы не включать заголовки несколько раз.

Для зависимых заголовков (а не основного заголовка для библиотеки) я часто использую защиту заголовка, которая выглядит примерно так:

#ifdef FOO_BAR_BAZ_H
#error foo_bar_baz.h multiply included
#else
#define FOO_BAR_BAZ_H

// header body

#endif

Ответ 6

IIRC, #pragma ничего не является частью языка. И это проявляется на практике, много.

(edit: полностью согласен с тем, что система включения и связывания должна быть более ориентирована на новый стандарт, так как это одна из самых очевидных недостатков в "le this day and age" )

Ответ 7

Вероятно, вы можете избежать коллизий имен, не прибегая к случайным строкам, указав включенную охрану, чтобы содержать имя класса и пространства имен в файле.

Кроме того, #pragma один раз поддерживается как компиляторами MS, так и GCC в течение довольно долгого времени, так почему же вы беспокоитесь о том, что это не по стандарту ISO?

Ответ 8

Это не в C (не С++), потому что обновления с 1989 года являются комитетами. У участников разные цели, но никто не собирается проводить кампанию за стандартизацию функции, которая сохраняет только две строки на файл. Если вы хотите элегантности, выберите современный язык, который был разработан для него с самого начала; C был разработан для минимизации сложности компилятора.

Ответ 9

Прагматическое решение:

1) выберите некоторую согласованную политику защиты именования (например, путь к корню проекта + имя файла или другое, что вы выберете). Включите возможные исключения для стороннего кода.

2) напишите программу (простой python script), чтобы провести рекурсивно исходное дерево и убедиться, что охранники все конформны для политики. И всякий раз, когда охранники ошибаются, выведите diff (или sed script или что-то еще), которое пользователь может легко применить для исправления. Или просто попросите подтверждение и внесите изменения из той же программы.

3) заставить всех в проекте использовать его (скажем, перед отправкой в ​​исходное управление).

Ответ 10

Я думаю, что правильный способ разрешить несколько действий включать только специальную прагму и запретить множественное включение по умолчанию например:

#pragma allow_multiple_include_this_file

Итак, так как вы спросили, почему. Вы отправили свое предложение стандартным разработчикам?:) Я тоже не отправлю. Может ли быть причиной?