Avatar ·

#pragma once против include guards: что выбрать для защиты от повторного включения?

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

// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b) { return a + b; }

#endif

// main.cpp
#include "math_utils.h"
#include "math_utils.h"  // случайно включил дважды

int main() {
    return add(2, 3);
}

С include guards всё работает, но я заметил, что многие современные проекты (например, из Boost) используют #pragma once. Я прочитал, что это нестандартная директива, но она поддерживается всеми основными компиляторами.

Меня интересует:

  • Есть ли реальные проблемы с производительностью у include guards?
  • Почему #pragma once считается более "чистым" решением?
  • Может ли #pragma once привести к ошибкам, если файл скопирован или находится в разных папках?
  • Что предпочитают в продакшн-коде: Google, Microsoft, Qt?

Хочется понять, что выбрать для нового проекта, чтобы не ошибиться.

<<>>

pragma once vs include guards: подробный разбор

Отличный вопрос! Давайте разберём оба подхода по косточкам.

1. Include guards (традиционный способ)

Это стандартный механизм, основанный на директивах препроцессора:

// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

// содержимое заголовка

#endif

Плюсы:

  • Полностью стандартизирован (работает на любом компиляторе C++)
  • Нет двусмысленности: гарантированно защищает от повторного включения
  • Можно использовать одно и то же имя guard в разных файлах (хотя это плохая практика)

Минусы:

  • Требуется придумывать уникальное имя макроса для каждого файла
  • Можно случайно допустить опечатку в имени макроса
  • Препроцессор всё равно читает файл целиком, проверяя #ifndef — это создаёт небольшую нагрузку
  • Код становится более многословным

2. #pragma once

Это нестандартная директива, поддерживаемая всеми основными компиляторами (MSVC, GCC, Clang, ICC):

// myheader.h
#pragma once

// содержимое заголовка

Плюсы:

  • Минимум кода — одна строка вместо трёх
  • Нет риска опечаток в имени макроса
  • Компилятор может оптимизировать: если файл уже был включён, он даже не открывает его повторно
  • Упрощает рефакторинг — не нужно переименовывать guard при переименовании файла

Минусы:

  • Не является частью стандарта C++ (но де-факто поддерживается везде)
  • Может давать сбои, если один и тот же файл доступен по разным путям (символические ссылки, жёсткие ссылки, монтирование)
  • Проблемы с копированием файлов: если скопировать .h файл в другую папку, компилятор может посчитать их разными, хотя содержимое идентично

3. Что выбрать?

Для новых проектов: #pragma once — предпочтительный вариант. Все современные компиляторы его поддерживают, и он чище.

Для библиотек с максимальной переносимостью: include guards. Если вы пишете библиотеку, которая должна компилироваться даже на древних компиляторах (например, для встраиваемых систем), используйте guards.

Гибридный подход (используется в Qt и некоторых проектах):

#pragma once
#ifndef MYHEADER_H
#define MYHEADER_H

// содержимое

#endif

Это даёт и оптимизацию #pragma once, и гарантию стандарта.

4. Что говорят крупные компании?

  • Google (Chromium, Abseil): используют include guards с определённым стилем имён
  • Microsoft (Windows SDK, MSVC): активно используют #pragma once
  • Qt: используют гибридный подход
  • Boost: в основном include guards (из-за поддержки старых компиляторов)
  • LLVM/Clang: используют include guards

5. Производительность

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

Итоговая рекомендация: для 99% проектов используйте #pragma once. Если вы пишете библиотеку, которая должна работать на 20-летних компиляторах — используйте include guards.

<<>>

Дополнение: как правильно организовать защиту включения

Хочу добавить несколько практических советов, которые помогут избежать типичных ошибок.

1. Именование include guards

Если вы выбрали include guards, используйте единый стиль. Лучший вариант — полный путь к файлу от корня проекта:

// myproject/utils/math.h
#ifndef MYPROJECT_UTILS_MATH_H_
#define MYPROJECT_UTILS_MATH_H_
// ...
#endif

Это гарантирует уникальность и упрощает поиск.

2. Проблема с копированием файлов

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

#include "config.h"
#include "../common/config.h"

И это один и тот же файл (через симлинк), #pragma once может не сработать. Include guards в этой ситуации надёжнее.

3. Современный C++20: модули

С появлением модулей в C++20 проблема защиты включения вообще исчезает:

// math.ixx
export module math;
export int add(int a, int b) { return a + b; }

Модули не требуют ни guards, ни #pragma once. Но это пока не везде поддерживается.

4. Практический совет

Начните с #pragma once. Если ваш проект когда-нибудь столкнётся с компилятором, который его не поддерживает (маловероятно), вы легко добавите include guards. Не усложняйте код заранее.