#pragma однажды против включения охраны? - PullRequest
325 голосов
/ 17 июля 2009

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

Примечание: для ускорения компиляции мы могли бы использовать Redundant Include Guards , но это добавляет тесную связь между включенным файлом и включаемым файлом. Обычно это нормально, потому что защита должна основываться на имени файла и будет меняться только в том случае, если вам все равно потребуется изменить имя включения.

Ответы [ 13 ]

284 голосов
/ 17 июля 2009

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

#pragma once менее подвержен ошибкам и меньше кода для ввода.

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

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

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

158 голосов
/ 22 июля 2011

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

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

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

101 голосов
/ 19 января 2016

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

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

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

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

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

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

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

33 голосов
/ 17 июля 2009

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

#ifndef BLAH_H
#define BLAH_H
#pragma once

// ...

#endif

Причины:

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

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

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

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

30 голосов
/ 13 ноября 2014

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

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

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

Почему #ifndef?

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

хиджры

#ifndef A_H
#define A_H

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

#endif

b.h

#ifndef B_H
#define B_H

#include "a.h"

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

#endif

c.h

#ifndef C_H
#define C_H

#include "b.h"

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

#endif

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

издевается / 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 дважды, один раз оригинал и один раз поддельная версия.

22 голосов
/ 17 июля 2009

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

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

#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_

...

#endif
18 голосов
/ 20 сентября 2016

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

15 голосов
/ 17 июля 2009

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

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

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

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

10 голосов
/ 15 октября 2015

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

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

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

9 голосов
/ 20 июля 2009

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...