#pragma однажды против включения охраны?

Я работаю над базой кода, которая, как известно, работает только на окнах и компилируется в Visual Studio (она тесно интегрируется с Excel, поэтому никуда не денется). Я задаюсь вопросом, должен ли я пойти с традиционными включенными охранниками или использовать #pragma once для нашего кода. Я думаю, что позволить компилятору разобраться с #pragma once приведет к более быстрой компиляции и менее подвержен ошибкам при копировании и вставке. Это также немного менее уродливо ;)

Примечание: чтобы ускорить компиляцию, мы могли бы использовать http://www.c2.com/cgi/wiki?RedundantIncludeGuards, но это добавляет тесную связь между включенным файлом и включаемым файлом. Обычно это нормально, потому что защита должна основываться на имени файла и будет меняться только в том случае, если вам все равно нужно изменить имя включения.

13 ответов

Решение

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

#pragma once менее склонен к ошибкам и меньше набирает код.

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

Я предпочитаю использовать #pragma once,

Смотрите эту статью в Википедии о возможности использования обоих.

Я просто хотел добавить к этому обсуждению, что я просто компилирую на VS и GCC, и раньше использовал include guard. Теперь я перешел на #pragma onceи единственная причина для меня - это не производительность, не переносимость и не стандарт, поскольку меня не волнует, что является стандартом, если его поддерживают VS и GCC, а именно:

#pragma onceуменьшает возможности для ошибок.

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

#pragma once имеет неустранимые ошибки Это никогда не должно использоваться.

Если твой #include Путь поиска достаточно сложен, компилятор может не определить разницу между двумя заголовками с одинаковым базовым именем (например, a/foo.h а также b/foo.h), так что #pragma once в одном из них будут подавлены оба. Также может быть невозможно сказать, что два разных родственника включают (например, #include "foo.h" а также #include "../a/foo.h" обратитесь к тому же файлу, так #pragma once не сможет подавить избыточное включение, когда оно должно иметь.

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

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

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

(Историческая справка: единственная причина, по которой я не разорвал #pragma once а также #import 12 лет назад, когда у меня были полномочия сделать это, GCC полагался на системные заголовки Apple. Оглядываясь назад, это не должно было остановить меня.)

(Поскольку это уже дважды упоминалось в ветке комментариев: разработчики GCC приложили немало усилий, чтобы #pragma once максимально надежный; см. отчет об ошибке GCC 11569. Однако реализация в текущих версиях GCC все еще может потерпеть неудачу при вероятных условиях, таких как создание ферм, страдающих от перекоса часов. Я не знаю, на что похожа реализация любого другого компилятора, но я бы не ожидал, что кто-то сделает лучше.)

До дня #pragma once становится стандартом (в настоящее время это не является приоритетом для будущих стандартов), я предлагаю вам использовать его И использовать охрану следующим образом:

#ifndef BLAH_H
#define BLAH_H
#pragma once

// ...

#endif

Причины:

  • #pragma once не является стандартным, поэтому возможно, что некоторые компиляторы не обеспечивают функциональность. Тем не менее, все основные компилятор поддерживает это. Если компилятор этого не знает, по крайней мере, он будет проигнорирован.
  • Так как нет стандартного поведения для #pragma once, вы не должны предполагать, что поведение будет одинаковым на всех компиляторах. Охрана обеспечит, по крайней мере, базовое допущение одинаково для всех компиляторов, которые, по крайней мере, реализуют необходимые инструкции препроцессора для охранников.
  • На большинстве компиляторов #pragma once ускорит компиляцию (одного cpp), потому что компилятор не откроет файл, содержащий эту инструкцию. Поэтому наличие этого в файле может помочь или нет, в зависимости от компилятора. Я слышал, что g++ может выполнять такую ​​же оптимизацию при обнаружении охранников, но это нужно подтвердить.

Используя два вместе, вы получаете лучшее из каждого компилятора для этого.

Теперь, если у вас нет автоматического сценария для создания охранников, может быть удобнее использовать #pragma once, Просто знайте, что это значит для переносимого кода. (Я использую VAssistX для быстрой генерации защиты и прагмы)

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

С точки зрения тестировщика программного обеспечения

#pragma once он короче, чем защита включения, меньше подвержен ошибкам, поддерживается большинством компиляторов, а некоторые говорят, что он компилируется быстрее (что не так [больше]).

Но я все же предлагаю вам пойти со стандартным #ifndef включить охранников.

Зачем #ifndef?

Рассмотрим надуманную иерархию классов, подобную этой, где каждый из классов A, B, а также C живет внутри своего собственного файла:

ах

#ifndef A_H
#define A_H

class A {
public:
  // some virtual functions
};

#endif

ЬН

#ifndef B_H
#define B_H

#include "a.h"

class B : public A {
public:
  // some functions
};

#endif

ч

#ifndef C_H
#define C_H

#include "b.h"

class C : public B {
public:
  // some functions
};

#endif

Теперь давайте предположим, что вы пишете тесты для своих классов, и вам нужно смоделировать поведение действительно сложного класса B, Один из способов сделать это - написать фиктивный класс, используя, например, Google Mock, и поместить его в каталог. mocks/b.h, Обратите внимание, что имя класса не изменилось, но оно хранится только в другом каталоге. Но что наиболее важно, так это то, что include guard назван точно так же, как и в оригинальном файле. b.h,

издевается / ЬН

#ifndef B_H
#define B_H

#include "a.h"
#include "gmock/gmock.h"

class B : public A {
public:
  // some mocks functions
  MOCK_METHOD0(SomeMethod, void());
};

#endif

В чем выгода?

При таком подходе вы можете издеваться над поведением класса B не касаясь исходного класса или рассказывая C об этом. Все, что вам нужно сделать, это положить каталог mocks/ в пути включения вашего компилятора.

Почему это не может быть сделано с #pragma once?

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

После участия в расширенном обсуждении предполагаемого компромисса производительности между #pragma once а также #ifndef Охранники против аргумента правильности или нет (я был на стороне #pragma once основываясь на некоторой относительно недавней идеологической обработке), я решил наконец проверить теорию, что #pragma once быстрее, потому что компилятор не должен пытаться повторно#include файл, который уже был включен.

Для теста я автоматически сгенерировал 500 заголовочных файлов со сложными взаимозависимостями и имел .c файл, который #includeС их всех. Я выполнил тест тремя способами, один раз просто #ifndefоднажды с просто #pragma onceи один раз с обоими. Я выполнил тест на довольно современной системе (MacBook Pro 2014 года, работающий под управлением OSX, использующий Clang в комплекте с XCode, с внутренним SSD).

Сначала тестовый код:

#include <stdio.h>

//#define IFNDEF_GUARD
//#define PRAGMA_ONCE

int main(void)
{
    int i, j;
    FILE* fp;

    for (i = 0; i < 500; i++) {
        char fname[100];

        snprintf(fname, 100, "include%d.h", i);
        fp = fopen(fname, "w");

#ifdef IFNDEF_GUARD
        fprintf(fp, "#ifndef _INCLUDE%d_H\n#define _INCLUDE%d_H\n", i, i);
#endif
#ifdef PRAGMA_ONCE
        fprintf(fp, "#pragma once\n");
#endif


        for (j = 0; j < i; j++) {
            fprintf(fp, "#include \"include%d.h\"\n", j);
        }

        fprintf(fp, "int foo%d(void) { return %d; }\n", i, i);

#ifdef IFNDEF_GUARD
        fprintf(fp, "#endif\n");
#endif

        fclose(fp);
    }

    fp = fopen("main.c", "w");
    for (int i = 0; i < 100; i++) {
        fprintf(fp, "#include \"include%d.h\"\n", i);
    }
    fprintf(fp, "int main(void){int n;");
    for (int i = 0; i < 100; i++) {
        fprintf(fp, "n += foo%d();\n", i);
    }
    fprintf(fp, "return n;}");
    fclose(fp);
    return 0;
}

А теперь мои различные тестовые прогоны:

folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.164s
user    0m0.105s
sys 0m0.041s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.140s
user    0m0.097s
sys 0m0.018s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.193s
user    0m0.143s
sys 0m0.024s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.153s
user    0m0.101s
sys 0m0.031s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.170s
user    0m0.109s
sys 0m0.033s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.155s
user    0m0.105s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.153s
user    0m0.101s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.181s
user    0m0.133s
sys 0m0.020s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.167s
user    0m0.119s
sys 0m0.021s
folio[~/Desktop/pragma] fluffy$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/c++/4.2.1
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin17.0.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

Как видите, версии с #pragma once действительно немного быстрее для предварительной обработки, чем #ifndef- только один, но разница была весьма незначительной, и ее намного затмило бы количество времени, которое потребовалось бы для построения и связывания кода. Возможно, с достаточно большой базой кода это может привести к разнице во времени сборки в несколько секунд, но между современными компиляторами, способными оптимизировать #ifndef Охрана, тот факт, что операционные системы имеют хорошие дисковые кеши и растущие скорости технологии хранения данных, кажется, что аргумент производительности является спорным, по крайней мере, для типичной системы разработчиков в наше время. Более старые и более экзотические среды сборки (например, заголовки, размещенные на общем сетевом ресурсе, сборка с ленты и т. Д.) Могут несколько изменить уравнение, но в этих обстоятельствах представляется более полезным просто создать менее хрупкую среду сборки в первую очередь.

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

Таким образом, хотя #pragma once заметно (немного) быстрее, я не согласен, что это само по себе является причиной для использования его #ifndef охранники.

РЕДАКТИРОВАТЬ: Благодаря обратной связи от @LightnessRacesInOrbit я увеличил количество заголовочных файлов и изменил тест, чтобы он выполнял только шаг препроцессора, исключая любое небольшое количество времени, добавляемое процессом компиляции и компоновки (что было тривиально до и несуществующий сейчас). Как и ожидалось, дифференциал примерно одинаков.

Если вы уверены, что никогда не будете использовать этот код в компиляторе, который его не поддерживает (Windows/VS, GCC и Clang являются примерами компиляторов, которые его поддерживают), то вы, безусловно, можете использовать #pragma один раз, не беспокоясь,

Вы также можете просто использовать оба (см. Пример ниже), так что вы получите ускорение переносимости и компиляции на совместимых системах

#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_

...

#endif

Есть связанный вопрос, на который я ответил:

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

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

Я вообще не заморачиваюсь #pragma once поскольку мой код иногда должен компилироваться с чем-то отличным от MSVC или GCC (компиляторы для встроенных систем не всегда имеют #pragma).

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

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

В любом случае, мне просто проще использовать охранники #include, которые будут работать везде и больше не беспокоиться об этом.

Я думаю, что первое, что вы должны сделать, это проверить, действительно ли это изменит ситуацию, т.е. Вы должны сначала проверить производительность. Один из поисков в Google подбросил это.

На странице результатов столбцы для меня немного отключены, но ясно, что, по крайней мере, до VC6 Microsoft не реализовывал оптимизацию включения защиты, которую использовали другие инструменты. Когда внутренняя защита была внутренней, это занимало в 50 раз больше времени по сравнению с внешней защитой (внешние защитные элементы по крайней мере так же хороши, как #pragma). Но давайте рассмотрим возможные последствия этого:

Согласно представленным таблицам время открытия и проверки включения в 50 раз больше, чем у эквивалента #pragma. Но фактическое время для этого измерялось в 1 микросекунде на файл еще в 1999 году!

Итак, сколько дублирующих заголовков будет иметь один TU? Это зависит от вашего стиля, но если мы скажем, что средний TU имеет 100 дубликатов, то в 1999 году мы потенциально платим 100 микросекунд за TU. С улучшениями жесткого диска это, вероятно, значительно ниже, но даже тогда с предварительно скомпилированными заголовками и корректным отслеживанием зависимостей общая совокупная стоимость этого для проекта почти наверняка будет незначительной частью вашего времени сборки.

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

Нет никаких причин, по которым Microsoft не могла бы реализовать оптимизацию включения защиты таким же образом, как это делает GCC и любой другой компилятор (на самом деле кто-нибудь может подтвердить, реализуют ли их более поздние версии это?). ПО МОЕМУ МНЕНИЮ, #pragma once делает очень мало, кроме как ограничить ваш выбор альтернативного компилятора.

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

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

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

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

OT: если у вас есть другие советы / опыты по ускорению сборки, мне было бы любопытно.

Поводом к объяснению Konrad Kleine выше.

Краткое описание:

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

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

  • Теперь, когда мы используем #ifndef XYZ_H для заголовков ответственность за поддержание зависимости заголовков лежит больше на разработчиках. Это означает, что всякий раз, когда из-за какого-то нового заголовочного файла существует вероятность циклической зависимости, компилятор просто отметит некоторые "undefined .."сообщения об ошибках во время компиляции, и пользователь должен проверить логическое соединение / поток объектов и исправить неправильные включения.

Это определенно добавит к времени компиляции (так как необходимо исправить и перезапустить). Кроме того, поскольку он работает на основе включения файла, основываясь на определенном состоянии "XYZ_H", и по-прежнему жалуется, если не может получить все определения.

Поэтому, чтобы избежать подобных ситуаций, мы должны использовать, как;

#pragma once
#ifndef XYZ_H
#define XYZ_H
...
#endif

т.е. сочетание обоих.

Для тех, кто хотел бы использовать #pragma один раз и включить охранников вместе: если вы не используете MSVC, то вы не получите много оптимизации от #pragma один раз.

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

Вот подробное обсуждение с примерами использования #pragma.

Другие вопросы по тегам