Введение
Мы рассматривали include guard вкратце — в этой статье разберём подробно, почему он необходим, как именно проблема множественного включения проявляется без него, и сравним классический подход (#ifndef/#define/#endif) с более современным, но менее переносимым #pragma once.
Концепция
Директива #include текстуально вставляет полное содержимое указанного файла в точку включения — если один и тот же заголовочный файл оказывается включённым более одного раза в пределах одной единицы трансляции, например, через цепочку из нескольких разных заголовочных файлов, каждый из которых сам включает один и тот же общий заголовок, его содержимое будет вставлено (и, соответственно, скомпилировано) несколько раз, что для объявлений типов/классов приводит к ошибке компиляции о повторном определении. Include guard — паттерн препроцессорных директив (#ifndef HEADER_NAME / #define HEADER_NAME / … / #endif), гарантирующий, что реальное содержимое заголовочного файла будет фактически включено (и, соответственно, скомпилировано) только при первом включении в пределах конкретной единицы трансляции, тогда как последующие попытки включения того же файла будут эффективно проигнорированы благодаря уже установленному соответствующему макросу. #pragma once — более современная, краткая директива, специфичная для конкретного компилятора (хотя широко поддерживаемая всеми основными современными компиляторами), достигающая того же результата без необходимости вручную придумывать и поддерживать уникальное имя макроса для каждого файла.
Пример кода
// БЕЗ include guard — демонстрация проблемы множественного включения
// shape.h (БЕЗ защиты от повторного включения)
struct Shape {
int sides;
};
// triangle.h
#include "shape.h"
struct Triangle : Shape { /* ... */ };
// square.h
#include "shape.h"
struct Square : Shape { /* ... */ };
// main.cpp
#include "triangle.h" // включает shape.h ВНУТРИ
#include "square.h" // снова включает shape.h ВНУТРИ — ОШИБКА: struct Shape определена ПОВТОРНО!
// shape.h — С классическим include guard
#ifndef SHAPE_H
#define SHAPE_H
struct Shape {
int sides;
};
#endif // SHAPE_H
// При повторном включении этого файла #ifndef SHAPE_H окажется ЛОЖНЫМ (макрос уже определён) —
// реальное содержимое между #ifndef и #endif будет просто ПРОПУЩЕНО при повторном включении
// shape.h — современная альтернатива через #pragma once
#pragma once
struct Shape {
int sides;
};
// Значительно короче классического include guard, не требует придумывания уникального имени макроса,
// но является РАСШИРЕНИЕМ конкретных компиляторов, а не частью официального стандарта C++
// (хотя поддерживается практически ВСЕМИ современными, широко используемыми компиляторами)
Пояснения к коду
Пример без include guard явно показывает реальный механизм возникновения проблемы — main.cpp включает и triangle.h, и square.h, каждый из которых независимо включает shape.h, и без защиты от повторного включения итоговый, развёрнутый препроцессором текст main.cpp содержал бы определение struct Shape ДВАЖДЫ, что компилятор C++ отклоняет как ошибку повторного определения того же типа в пределах одной единицы трансляции. Классический include guard с #ifndef SHAPE_H/#define SHAPE_H/#endif решает эту проблему через простую логику — макрос SHAPE_H определяется при первом включении файла, и при попытке повторного включения проверка #ifndef SHAPE_H (означающая «если SHAPE_H ещё НЕ определён») оказывается ложной, и весь блок между #ifndef и соответствующим #endif просто пропускается препроцессором при повторных попытках включения. #pragma once достигает того же эффекта значительно компактнее, без необходимости вручную придумывать (и гарантировать уникальность в пределах всего проекта) конкретное имя макроса для каждого отдельного заголовочного файла, хотя технически является нестандартным (хотя фактически универсально поддерживаемым) расширением.
Подводные камни
- Случайное использование одинакового имени макроса include guard в двух разных заголовочных файлах (особенно вероятное при копировании содержимого одного заголовочного файла как шаблона для другого без должного внимания к переименованию макроса) — это приведёт к тому, что второй файл с тем же именем макроса include guard будет ошибочно считаться «уже включённым» сразу после включения первого файла, даже если они физически разные, что приведёт к тому, что реальное содержимое второго файла никогда не будет фактически включено, давая трудно диагностируемые ошибки об отсутствующих объявлениях, на первый взгляд необъяснимые без понимания истинной причины.
- Полное отсутствие защиты от множественного включения в заголовочном файле, содержащем определения типов/классов/функций (а не только декларации, для которых множественное включение менее проблематично, хотя всё же может создавать ненужную избыточность) — как явно показано в первом примере, это приводит к ошибке компиляции при попытке включить такой заголовочный файл (прямо или транзитивно, через другие заголовки) более одного раза в одной единице трансляции, что почти неизбежно происходит в любом нетривиальном, многофайловом проекте.
- Использование исключительно
#pragma onceв проекте, который должен компилироваться компилятором, не гарантированно поддерживающим эту директиву (хотя на практике это крайне редкая ситуация для современной разработки, где все основные компиляторы давно её поддерживают) — для максимальной, гарантированной переносимости (особенно для библиотек, предназначенных для широкого, не полностью предсказуемого круга пользователей с потенциально нестандартными инструментами сборки) классический#ifndef/#define/#endifinclude guard остаётся более универсальным, гарантированно переносимым выбором, хотя для подавляющего большинства современных проектов разница на практике непринципиальна. - Включение заголовочного файла, который сам не содержит include guard, внутри другого заголовочного файла (а не только внутри
.cpp-файла), что усугубляет вероятность проблемы множественного включения транзитивно, через цепочку из нескольких заголовков, как явно показано в исходном, проблемном примере — каждый заголовочный файл, потенциально включаемый из нескольких разных мест проекта (прямо или транзитивно), должен иметь собственную защиту от множественного включения независимо от того, кажется ли разработчику в моменте, что конкретно этот файл вряд ли будет включён более одного раза.