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

Программы C/C++ разделены на заголовок (.h, .hpp) и исходный код (.c, .cc, .cpp) и заголовочные файлы обычно включаются из одного или нескольких заголовочных и исходных файлов. Необходимо соблюдать осторожность, чтобы избежать переопределения типов и директив препроцессора. Каждый программист знает, как использовать защиту заголовков в качестве стандартного способа сделать это.

Директива #pragma once — это более новый метод для этой цели, который обеспечивает более высокую производительность компилятора, поскольку компилятору не нужно снова читать и предварительно обрабатывать файл заголовка. Другие преимущества включают улучшенную читаемость кода, меньшее количество ошибок, возникающих при перемещении или переименовании файлов заголовков, и отсутствие загромождения пространства имен препроцессора. Хотя он не включен в стандарт, все основные компиляторы C/C++ уже его поддерживают. GCC и Clang утверждают, что они способны обнаруживать идиому защиты заголовка и предлагают ту же производительность, что и #pragma Once, что делает ее ненужной. С другой стороны, Visual Studio поощряет использование этой директивы вместо защиты заголовков.

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

Я создал следующий заголовочный файл, который содержит чередующийся код в зависимости от значения определения препроцессора. Только один путь содержит директиву #pragma Once. Строки #pragma message являются заполнителями для фактического исходного кода, поэтому мы ясно увидим, какие строки обрабатываются из вывода компилятора.

// a.h
#ifdef HEAD
# pragma message ("Head included")
#else
# pragma once
# pragma message ("Body included")
#endif

Чтобы проверить это, я подготовил исходный файл, который включает файл заголовка несколько раз с разными значениями для определения препроцессора, управляющего файлом заголовка. Как и в другом файле, я добавил директивы #pragma message, чтобы компилятор выдавал более разумный вывод.

// a.c
#define HEAD
# pragma message ("Including head")
# include "a.h"
# pragma message ("Including head")
# include "a.h"
#undef HEAD
#pragma message ("Including body")
#include "a.h"
#pragma message ("Including body")
#include "a.h"
#define HEAD
# pragma message ("Including head")
# include "a.h"
#undef HEAD

Я скомпилировал эти файлы с помощью различных компиляторов (Clang 3.4, GCC 4.8.3 и MSVC 18), и все они работали должным образом. Я показываю только вывод MSVC, так как он самый читаемый, но все компиляторы вели себя одинаково:

a.c
Including head
Head included
Including head
Head included
Including body
Body included
Including body
Including head

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

Первоначально опубликовано на https://peterbudai.eu 12 октября 2014 г.