#pragma once vs include охранники?

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

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

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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

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

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

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

Ответ 4

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

#ifndef BLAH_H
#define BLAH_H
#pragma once

// ...

#endif

Причины:

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

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

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

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

Ответ 5

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

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

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

Почему #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. Один из способов сделать это - написать mock class, используя, например, google mock и поместить его в каталог mocks/b.h. Обратите внимание, что имя класса не изменилось, но оно хранилось только в другом каталоге. Но самое главное, что охранник include называется точно таким же, как в исходном файле 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 в два раза, после оригинальной и после изделенной версии.

Ответ 6

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

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

#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_

...

#endif

Ответ 7

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

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

EDIT: благодаря обратной связи от @LightnessRacesInOrbit я увеличил количество файлов заголовков и изменил тест, чтобы выполнить только шаг препроцессора, исключая любое небольшое количество времени, добавляемое процессом компиляции и связывания (который был тривиально раньше и не существует). Как и ожидалось, дифференциал примерно одинаковый.

Ответ 8

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

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

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

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

Ответ 9

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

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

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

Ответ 10

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

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

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

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

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

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

Ответ 11

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

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

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

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

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

Ответ 12

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

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

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

Ответ 13

Напоминание Конрада Клейна выше.

Краткий обзор:

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

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

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

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

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

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

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

Ответ 14

Я работаю над большим коммерческим С++-приложением, созданным в MSVC и XCode/Clang. Мы значительно улучшили время сборки, переключившись с #ifndef на #pragma once. Я найду измеренное ускорение и отредактирую этот ответ, когда завтра приеду.