Skip to content
VLSI Mentor

C++ Part I · Module 24 · C++ Build and Project Architecture

Include Guards and #pragma once

A header gets included more than once almost immediately. Two headers both include a third; a header includes a header that includes it back; a source file includes a convenience header and then a specific one. Since Chapter 24.1 established that at most one definition of a class may appear in a translation unit, repeated inclusion is a build error by default, and the mechanism that prevents it is one of the first things anyone learns in C++. What is much less widely known is what each mechanism actually promises. An include guard is a macro, and macros have no namespace, so two unrelated headers that happen to choose the same guard name produce a failure in which one header silently expands to nothing and the compiler reports an error in the file that used it. The pragma cannot collide, because it identifies files rather than names — but identifying files is itself an assumption, and it fails where the same header reaches the compiler by two different paths. This chapter measures all of it, including the two cases that are not symmetric, so the choice between them is informed rather than tribal.

Intermediate17 min readC++Part IFundamentalsBuild

Part I · Module 24 · Chapter 24.3 · Include Guards and #pragma once

1. Why Repeated Inclusion Happens, and What It Costs

A header included twice in one translation unit means its contents appear twice. For declarations that is harmless — a declaration may be repeated. For a definition it is a violation of Chapter 24.1 §2's second part, which allows at most one definition per translation unit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// noguard.h
struct Reg { int v; };

// n1.cpp
#include "noguard.h"
#include "noguard.h"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
In file included from n1.cpp:2:
./noguard.h:1:8: error: redefinition of 'Reg'
    1 | struct Reg { int v; };
      |        ^
n1.cpp:1:10: note: './noguard.h' included multiple times, additional include site here

The diagnostic is good. It names the type, it names the header, and the note names both include sites. Clang recognises the pattern specifically and says so.

That is the direct case, which nobody writes on purpose. The realistic cases are indirect:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bus.h    -> types.h
report.h -> types.h
main.cpp -> bus.h, report.h        types.h arrives twice

With a three-header project that is already unavoidable, which is why every header needs a guard — not as a style rule but because the alternative does not compile.

2. The Include Guard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#ifndef VM_BUS_H
#define VM_BUS_H
// … contents …
#endif

The mechanism is exactly what it looks like. The first inclusion finds VM_BUS_H undefined, defines it, and processes the contents. Every later inclusion finds it defined and skips to the #endif. Measured on the same double include, the compiler emits no diagnostics at all — which is what success looks like here, and is worth saying explicitly because the rest of this chapter is about failures that also emit nothing.

Two properties are worth naming because they are the reasons it is still the recommended default.

It is standard C++, with no implementation-defined behaviour. Every compiler that exists handles it identically, because it is just #ifndef.

Compilers optimise it. A header whose entire contents are wrapped in a guard is recognised, and the compiler remembers the macro so that later inclusions are skipped without re-opening the file. This is the include guard optimisation, and it means the usual claim that #pragma once is faster has been largely untrue for a long time. The condition is that the guard must wrap the whole file — a stray declaration before the #ifndef defeats the recognition.

The cost is the one in §3: the guard is a macro name, and macro names are global.

3. The Guard-Name Collision

Two headers, in different directories, written by different people, both reaching for the obvious name:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// bus/config.h
#ifndef CONFIG_H
#define CONFIG_H
struct BusConfig { int width; };
inline int busWidth() { return 32; }
#endif

// mem/config.h
#ifndef CONFIG_H
#define CONFIG_H
struct MemConfig { int banks; };
inline int memBanks() { return 8; }
#endif
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include "bus/config.h"
#include "mem/config.h"
int main() { BusConfig b{32}; MemConfig m{8}; return b.width + m.banks; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
n3.cpp:3:31: error: unknown type name 'MemConfig'; did you mean 'BusConfig'?
    3 | int main() { BusConfig b{32}; MemConfig m{8}; return b.width + m.banks; }
      |                               ^~~~~~~~~
      |                               BusConfig
./bus/config.h:3:8: note: 'BusConfig' declared here
    3 | struct BusConfig { int width; };
      |        ^
n3.cpp:3:66: error: no member named 'banks' in 'BusConfig'

The second header expanded to nothing, because CONFIG_H was already defined by the first. And look at where the compiler points: at line 3 of n3.cpp, the file that used the type, with a helpful suggestion to use BusConfig instead. It even points at bus/config.h as the place BusConfig was declared — which is true and completely beside the point.

Nothing in the diagnostic mentions mem/config.h at all. The file that failed to contribute anything is not named, the duplicated macro is not named, and the suggestion actively pushes you toward the wrong fix. A developer seeing this will check the include path, check the spelling of MemConfig, check for a missing namespace, and eventually find it by preprocessing the file by hand:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clang++ -std=c++20 -E n3.cpp | grep -c MemConfig     # 0

The frequency of this in real codebases is higher than it looks, because the obvious guard names are obvious to everyone: CONFIG_H, UTILS_H, TYPES_H, COMMON_H, LOG_H. Any two libraries each with a config.h will collide the moment one translation unit includes both.

The fix is a naming scheme that cannot collide:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#ifndef VM_BUS_CONFIG_H          // project _ path _ file
#define VM_BUS_CONFIG_H

Project prefix, then the path, then the filename. A guard matching the file's path within the project is mechanically checkable, and §7 shows the check.

4. #pragma once

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#pragma once
// … contents …

Not in the standard, and supported by every compiler anyone uses — GCC, Clang, MSVC, ICC. It asks the preprocessor to include this file at most once per translation unit, which is a statement about files rather than about names.

On §3's collision the same two headers, with #pragma once instead of the shared guard macro, compile with no diagnostics and both types are available.

It cannot collide, because there is no name to collide. That is the real advantage, and it is a correctness advantage rather than the performance one usually claimed.

It has its own failure, which is the price of identifying files:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
In file included from n5.cpp:2:
./copy.h:2:8: error: redefinition of 'OnlyOnce'
    2 | struct OnlyOnce { int v; };
      |        ^
./real.h:2:8: note: previous definition is here

A copy of a header is a different file, so #pragma once does not deduplicate it, and you get the redefinition error the guard would have prevented. That sounds contrived until you consider the ways it happens in practice: a vendored third-party header that also exists in a system include path, a generated header written into two build directories, a file present in both a source tree and an install tree, or two build systems that each copy headers into their own staging area.

The complementary case is better than its reputation. Replacing the copy with a symlink to the same file and including both paths compiles with no diagnostics.

A symlink to the same file is correctly recognised as the same file. Clang resolves it. Hard links and symlinks are the cases implementations do handle; what they cannot handle is genuinely distinct files with identical contents, since detecting that would require comparing contents rather than identity.

5. Using Both

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#pragma once
#ifndef VM_BUS_CONFIG_H
#define VM_BUS_CONFIG_H

#include "vm/types.h"

namespace vm {
struct BusConfig { int width; };
}

#endif  // VM_BUS_CONFIG_H

Three details make this correct rather than merely conventional.

The guard wraps the entire file — nothing before the #ifndef, nothing after the #endif except the comment. This is what lets the compiler apply the include-guard optimisation from §2.

The guard name matches the path: vm/bus/config.h becomes VM_BUS_CONFIG_H. Derivable, unique, and checkable.

The #endif carries the macro name as a comment. In a 400-line header with nested #if blocks this is the difference between finding the matching #endif and guessing.

Two names to avoid entirely: anything starting with an underscore followed by a capital, and anything containing a double underscore. Those are reserved for the implementation, and _CONFIG_H is both a reserved name and a collision waiting to happen with a system header.

6. The Cases Guards Do Not Solve

An include guard prevents repeated inclusion. It does not make a header order-independent, and it does not solve circular dependency.

A circular include still fails, just differently. If a.h includes b.h and b.h includes a.h, then with guards in place the inner inclusion of a.h is skipped — so b.h is compiled with only the part of a.h that preceded the include. The result is usually an undeclared-type error inside b.h, pointing at a line that is correct. The fix is not a better guard; it is to break the cycle with a forward declaration, which is Chapter 24.4's subject.

A header that depends on include order still fails. Guards do not make a header self-sufficient. Chapter 24.2 §6 is the duty: every header compiles standalone, and the one-line -fsyntax-only check per header is how you enforce it.

The ODR violation in Chapter 24.1 §4 is untouched. Guards operate inside one translation unit. Two translation units with different definitions of one class are perfectly guarded and still wrong.

7. Checking the Rule Mechanically

A naming convention nobody checks is a naming convention that drifts. The guard name is derivable from the path, so the check is a few lines and belongs in the build:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# every header has a pragma, a guard matching its path, and nothing outside the guard
for h in $(find include -name '*.h'); do
  want=$(echo "$h" | sed 's|^include/||; s|[/.]|_|g' | tr 'a-z' 'A-Z')
  grep -q "^#ifndef $want\$" "$h" || echo "$h: guard is not $want"
  grep -q '^#pragma once$'     "$h" || echo "$h: missing #pragma once"
done

Two further checks are worth having, and both caught something real in this module's own test material:

Every header compiles standalone — the -fsyntax-only loop from Chapter 24.2 §6.

No two headers share a guard macro. Collect every #ifndef from every header, sort, and look for duplicates. That is the §3 failure, detected in one pass, and it is the check that would have turned an afternoon of confusion into a one-line build error.

8. The Comparison

include guard#pragma once
standardyesno, but universal in practice
identifies a header bya name you choseits identity on disk
can two headers collideyes — §3no
failure modefalse positive: a header silently expands to nothingfalse negative: included twice
what the failure looks likean error in the consumer, naming neither the header nor the macroredefinition of 'X', naming the type and both files
a copy of the same filededuplicated correctlynot deduplicated — §4
a symlink to the same filededuplicated correctlydeduplicated correctly (measured)
skipping cost on re-inclusionoptimised away if the guard wraps the fileoptimised away
boilerplatethree linesone line
checkable from the filenameyes — §7n/a

9. What to Actually Do

  • Put both on every header: #pragma once, then a path-derived #ifndef guard wrapping the whole file.
  • Derive the guard from the path — vm/bus/config.h becomes VM_BUS_CONFIG_H — so it is unique and checkable.
  • Never use a bare CONFIG_H, UTILS_H, TYPES_H or COMMON_H. Measured: the collision makes the second header expand to nothing and the compiler blames the consumer.
  • Never start a guard with an underscore and a capital, or contain a double underscore. Those are reserved.
  • Keep the guard wrapping the whole file so the include-guard optimisation applies.
  • Comment the #endif with the macro name.
  • Add three checks to the build: guard matches path, no two headers share a guard, every header compiles standalone.
  • When a header seems to be empty, preprocess it — clang++ -E file.cpp | grep TheType — before investigating anything else.
  • Fix a circular include with a forward declaration, not with a cleverer guard.

10. Mistakes Worth Making Once

#ifndef CONFIG_H in two different libraries. The second header vanishes and the error blames your main.

Taking the compiler's suggestion. It offers BusConfig where you wrote MemConfig, and the suggestion is wrong.

Copying a vendored header into the build tree while it also exists on the include path. #pragma once sees two files and the guard is what saves you.

A declaration before the #ifndef. It is included every time, and the include-guard optimisation no longer applies.

_VM_BUS_H. A reserved name.

Believing #pragma once is meaningfully faster. The include-guard optimisation has existed for a long time; the real argument for the pragma is that it cannot collide.

Expecting a guard to fix a circular include. It converts the error into a confusing one inside the other header.

Expecting a guard to prevent Chapter 24.1 §4's ODR violation. Guards work within one translation unit.

11. Interview Relevance

The frozen curriculum marks this chapter High, and the question is usually "include guards or #pragma once?" — which is often asked expecting a preference and is much better answered with the trade-off.

The answer that stands out names what each mechanism compares: a guard compares a name you chose, so two headers can collide and the second one silently expands to nothing; the pragma compares file identity, so it cannot collide but cannot see that a copy is the same header. Then the asymmetry: the guard's failure is a false positive that produces a misleading error in innocent code, the pragma's is a false negative that produces a clear redefinition error naming both files — so one fails in the right place and the other does not. Concluding with "use both, and derive the guard from the path so it is checkable" is a complete answer.

If the follow-up is about performance, knowing that the include-guard optimisation makes the usual speed claim obsolete is the detail that shows the answer is current.

12. What You Should Be Able to Say Now

  • Why repeated inclusion is a build error at all — Chapter 24.1 §2's at-most-one-definition-per-translation-unit — and that with three headers it is already unavoidable.
  • That a guard is a macro, that macro names are global, and that two headers sharing one makes the second expand to nothing.
  • That the resulting diagnostic names neither the skipped header nor the macro, points at the consumer, and suggests the wrong type.
  • That #pragma once cannot collide, which is a correctness advantage, not a speed one.
  • That #pragma once does not dedupe a copy of a header (measured: redefinition of 'OnlyOnce') and does dedupe a symlink (measured: clean).
  • That the two failures are opposite — false positive versus false negative — and why the false positive is the worse one.
  • That using both costs three lines and covers both failure modes.
  • That the include-guard optimisation exists, requires the guard to wrap the whole file, and undercuts the usual performance argument.
  • That guards do not fix circular includes, include-order dependencies, or cross-translation-unit ODR violations.

13. Exercises

Exercise 1 — the direct case. Include a header twice with no guard and read the full diagnostic, including the note. Add a guard and confirm it compiles.

Exercise 2 — the collision. Reproduce §3 exactly. Then, without looking at the headers, try to diagnose it from the error alone. Then run clang++ -E and grep for the missing type.

Exercise 3 — the pragma's failure. Put #pragma once in a header, copy the file, include both, and read the error. Then replace the copy with a symlink and confirm the difference.

Exercise 4 — the checks. Write §7's three checks and run them over a real project's include/ tree. Report what they find without fixing anything you were not asked to.

Exercise 5 — the circular include. Make a.h and b.h include each other, both properly guarded, and explain the resulting error in terms of §6.

14. Summary

  • Repeated inclusion is a build error because at most one definition of a class may appear in a translation unit, and with three headers in a project it happens by itself. Measured, the unguarded case gives error: redefinition of 'Reg' with a note naming both include sites.
  • An include guard compares a name you chose. Standard, universal, optimised by compilers when it wraps the whole file — and collidable.
  • The collision is the chapter's finding. Two headers both using CONFIG_H: the second expands to nothing, and the diagnostic is unknown type name 'MemConfig'; did you mean 'BusConfig'? — pointing at the consumer, naming neither the skipped header nor the duplicated macro, and suggesting the wrong fix.
  • #pragma once compares file identity, so it cannot collide. That is a correctness advantage; the usual speed argument is undercut by the include-guard optimisation.
  • Its failure is the opposite kind. A copy of a header is a different file and is not deduplicated — measured, redefinition of 'OnlyOnce' — while a symlink is correctly recognised as the same file.
  • The asymmetry decides it. A guard collision is a false positive that fails quietly in the wrong place; a pragma miss is a false negative that fails loudly in the right one. Use both, and derive the guard from the path so it is unique and mechanically checkable.
  • Both failures share a shape: a check that answers "have I seen this?" using a proxy for identity — a chosen name, or a path — rather than identity itself. The same shape as the linker's missing type symbols in Chapter 24.1 §4.
  • Guards do not solve circular includes, include-order dependence, or cross-translation-unit ODR violations.

Chapter 24.4 is the technique for not needing the include at all, which is both the fix for circular dependencies and the largest compile-time lever in the module.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.