C++ / NAMESPACES, HEADERS, AND BUILDS
Include guards, pragma once, and module basics
Protect every header from double inclusion with a guard or #pragma once, know which one breaks and why, and compile a minimal export module/import pair.
What you will learn
- Wrap a whole header in #ifndef/#define/#endif using a project-unique macro name
- Explain double inclusion as the same text pasted twice into one translation unit
- Choose #pragma once or a text guard by file identity versus macro identity
- Compile a two-file export module and import pair in dependency order
Understanding Include guards, pragma once, and module basics
The preprocessor handles #include by pasting the file's text at that point, so by the time the compiler proper starts, your .cpp file and everything it reached are one long translation unit. Inside a single translation unit a class, enum, template, or inline function may be defined exactly once, so when the same header's text arrives twice you get a redefinition error rather than a silently ignored duplicate. The second arrival is almost never something you wrote on purpose: panel.hpp and board.hpp both include widget.hpp, main.cpp includes both, and widget.hpp is reached twice. Because a header cannot know who else will pull it in, the protection has to live in the header itself.
A text include guard works by leaving a mark: the first pass defines the macro, and on any later pass the #ifndef is false, so the preprocessor deletes the whole body before the compiler ever sees it. The identity being compared is the macro name you chose, which makes the pattern immune to copied files but silently broken when two headers share a name. #pragma once shifts identity to the file itself, typically device and inode or a canonicalized path, which removes the naming problem but treats two copies of one header at different paths as two different headers. Speed is not a tiebreaker, since compilers recognize a fully guarded header and skip re-reading it, and neither form does anything about duplicate symbols across separate translation units, which is what inline is for.
Modules attack the cause instead of the symptom. An interface unit starting with export module geometry; is translated once into a compiled interface file, and import geometry; loads pre-parsed declarations rather than pasting text, so "included twice" stops being a possible state and there is nothing to guard. The rest follows from that: macros do not travel across an import, names you did not mark export stay invisible to importers, import order is irrelevant, and every interface must be compiled before its consumers, which is why module-aware builds must scan sources for dependencies. Legacy headers still work inside the global module fragment, the region between module; and the module declaration, which is how a module can use <cmath> without leaking it to importers.
Include guards are per-translation-unit text tricks, while modules replace textual inclusion with compiled, imported declarations.
<iostream>
// #include is pure text substitution, so pasting a header's text twice by
// hand is exactly what two indirect includes of that header would do.
// ---- first arrival of widget.hpp ----
DEMO_WIDGET_HPP
DEMO_WIDGET_HPP
struct Widget {
int id;
};
inline const char* widget_kind() { return "button"; }
// DEMO_WIDGET_HPP
// ---- second arrival of widget.hpp ----
DEMO_WIDGET_HPP
DEMO_WIDGET_HPP
struct Widget { // never reaches the compiler: the guard is defined
int id;
};
inline const char* widget_kind() { return "button"; }
// DEMO_WIDGET_HPP
int main() {
Widget w{7};
std::cout << widget_kind() << ' ' << w.id << '\n';
DEMO_WIDGET_HPP
std::cout << "second arrival was discarded by the guard\n";
}
#include copies text, so a header must make its own second copy vanish, whereas a module is imported as compiled declarations and cannot be duplicated at all.
Worked examples
Two headers sharing one guard name
Shows how a copy-pasted guard macro makes an entire header expand to nothing without any warning.
<iostream>
// ---- geometry.hpp ----
UTIL_H
UTIL_H
GEOMETRY_BODY_KEPT
inline int area(int w, int h) { return w * h; }
// ---- logging.hpp, made by copying geometry.hpp and editing the middle ----
UTIL_H // guard name was never changed
UTIL_H
LOGGING_BODY_KEPT
inline int log_level() { return 3; }
int main() {
GEOMETRY_BODY_KEPT
std::cout << "geometry.hpp: kept\n";
std::cout << "geometry.hpp: discarded\n";
LOGGING_BODY_KEPT
std::cout << "logging.hpp: kept\n";
std::cout << "logging.hpp: discarded\n";
std::cout << area(3, 4) << '\n';
// std::cout << log_level(); // would not compile: no such declaration
}
Example explained
Line 1The second #ifndef UTIL_H is false because the first block already defined UTIL_H, so everything down to its #endif is deleted.
Line 2LOGGING_BODY_KEPT is never defined, which is the only visible trace that a whole header contributed nothing.
Line 3Uncommenting the last line fails with an error about log_level, not about guards, so the include line still looks innocent.
Line 4Deriving the macro from the path, as in APP_LOGGING_HPP, makes this collision impossible.
The diamond that guards exist for
A real four-file layout where one header is reached twice through two unrelated headers.
// ---- widget.hpp ----
APP_WIDGET_HPP
APP_WIDGET_HPP
struct Widget { int id; };
// ---- panel.hpp ----
APP_PANEL_HPP
APP_PANEL_HPP
"widget.hpp"
inline int panel_id(Widget w) { return w.id * 10; }
// ---- board.hpp ----
APP_BOARD_HPP
APP_BOARD_HPP
"widget.hpp"
inline int board_id(Widget w) { return w.id + 100; }
// ---- main.cpp ----
<iostream>
"panel.hpp"
"board.hpp"
int main() {
Widget w{4};
std::cout << panel_id(w) << ' ' << board_id(w) << '\n';
}
// g++ -std=c++17 main.cpp -o app && ./app
Example explained
Line 1panel.hpp pulls in widget.hpp first and leaves APP_WIDGET_HPP defined for the rest of the translation unit.
Line 2board.hpp then opens widget.hpp again, but the file expands to zero tokens, so struct Widget is defined once.
Line 3g++ -E -P main.cpp | grep -c 'struct Widget' prints 1; remove widget.hpp's three guard lines and it prints 2 and the build fails.
Line 4Each header can include what it needs instead of trusting main.cpp's ordering, precisely because repeats cost nothing.
The same interface as a module
A minimal export module and import pair, showing that nothing is pasted and nothing needs guarding.
// ---- geometry.cppm : the module interface unit ----
module; // global module fragment: textual includes go here
<cmath>
export module geometry;
int rounded(double d) { return static_cast<int>(d + 0.5); } // module-local
export int hypotenuse(int a, int b) { return rounded(std::hypot(a, b)); }
export const char* unit() { return "px"; }
// ---- main.cpp ----
<iostream>
import geometry;
int main() {
std::cout << hypotenuse(3, 4) << unit() << '\n';
// rounded(1.4); // error: not exported, so not visible here
// std::sqrt(4.0); // error: <cmath> did not come along with the import
}
// GCC 14:
// g++ -std=c++20 -fmodules-ts -x c++ -c geometry.cppm
// g++ -std=c++20 -fmodules-ts main.cpp geometry.o -o app && ./app
Example explained
Line 1export module geometry; marks the file as the module's interface, and the compiler writes a compiled interface alongside the object file.
Line 2import geometry; reads those stored declarations, so no text is copied and repeating the import cannot duplicate anything.
Line 3rounded and everything from <cmath> stay behind the module boundary; the importer sees only the two exported functions.
Line 4The interface must be compiled first, so a build that compiles main.cpp before geometry.cppm fails with a missing module interface.
Important notes
#pragma once is an extension every mainstream compiler supports rather than standard C++, and it decides 'same file' from device and inode or a canonical path, so two copies of one header on disk defeat it while a text guard still works.
Module support is uneven: GCC 14 needs -fmodules-ts (plus -x c++ for a .cppm file), Clang needs --precompile and -fprebuilt-module-path=., and interfaces must be built before importers, so wildcard build rules break.
Common mistakes
Copying an existing header, editing the contents but not the guard macro: the new header expands to nothing, and the errors name the missing functions instead of the guard, so the include line looks correct.
Closing the guard early, or adding an #include or declaration after #endif: that tail is re-processed on every inclusion, producing redefinition errors in a header you believe is guarded.
Treating import like #include: macros from a module unit and the headers it includes never reach the importer, so code that compiled after an #include suddenly fails to find names after an import.
Try it yourself
Change, predict, then run
In one file, paste the same three-line guarded struct twice and run it, then delete only the #ifndef, #define, and #endif lines and compile again; write down the exact redefinition message and which of the two copies the compiler blames.
Open the C++ workspaceCheck your understanding
A build keeps third_party/inc/vec.hpp and also copies it to build/gen/vec.hpp, and one translation unit ends up reaching both paths. The header's only protection is #pragma once. What happens?
- It is processed once, because #pragma once compares the contents of files it has already read and the two copies are byte-identical.
- It is processed twice, but harmlessly, because #pragma once guarantees the second copy's definitions match the first.
- It is processed twice, so the class inside it is defined twice in that translation unit and the compile fails, because #pragma once skips only a file it recognizes as the same file.
- The compiler rejects #pragma once itself, since it is not part of standard C++.
Show answer
#pragma once records file identity, typically device and inode or a canonicalized path, so a second copy at another path is a different file and gets processed, defining the class twice in one translation unit. A text include guard would have stopped it, because the second copy tests a macro name that is already defined. Option 0 is tempting since the files are identical, but mainstream compilers compare identity, not text; option 3 is wrong because every major compiler accepts the pragma without a diagnostic.