An include guard stops a header being pasted into the same source file twice. It does not stop the header’s contents appearing in every source file that includes it — and the difference between those two sentences is where most header bugs live.
This guide covers the guard pattern itself, the two things it genuinely protects you from, the one thing people wrongly believe it protects them from, how #pragma once actually compares once you measure rather than repeat the folklore, and where C++20 modules leave both of them. Every program on this page was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 and GCC 14.2, using -std=c17 -Wall -Wextra unless noted; the examples this refresh replaces demonstrated #ifndef without ever showing a header file, so every code block here has been rewritten and run. Every output block, warning and linker error is captured verbatim.
What Are #ifndef and #define?
#ifndef is a preprocessor directive that tests whether a macro name has not been defined, and compiles the code up to the matching #endif if so. #define creates that macro name. Wrapping a header’s entire contents in #ifndef NAME / #define NAME / #endif produces an include guard: the first #include defines the macro and the body is processed, and any later #include of the same header in the same source file finds the macro already defined and skips the body. This all happens before compilation, in the preprocessor, and it is scoped to one translation unit.
That last clause is the one to hold on to. A translation unit is one .c or .cpp file plus everything it includes. The guard macro is created while that file is being preprocessed and is gone before the compiler starts on the next one.
.c file, and that is all it stops. Two source files each including the header once both receive whatever it defines, so a definition in a header still collides at link time. Declare with extern in the header and define once in a .c file.
The Include Guard Pattern
Here is a header with a guard, which is the shape you will write a thousand times:
/* counter.h */
#ifndef COUNTER_H
#define COUNTER_H
struct Counter {
int value;
};
void counter_bump(struct Counter *c);
#endif /* COUNTER_H */
And a source file that includes it twice, which happens constantly in real projects through indirect includes rather than by anyone writing it out:
/* a.c */
#include "counter.h"
#include "counter.h"
int main(void) { return 0; }
a.c compiles cleanly
Without the guard, struct Counter would be defined twice in one translation unit and the compiler would reject it. That is the guard’s job, and it does it well.
Three details worth getting right:
- The
#endifcomment is optional but earns its place. In a 400-line header,#endifon its own tells you nothing about which block it closes. - Nothing goes outside the guard. Not includes, not comments that matter, and definitely not code. Anything above
#ifndefor below#endifis processed on every inclusion. - The guard name must be unique across your entire project, not just the directory. The Guard Names section below shows what happens when it is not.
What a Guard Does Not Protect Against
This is the misconception worth an entire section, because it is stated confidently all over the internet: include guards do not prevent multiple definition. They prevent multiple inclusion, which is a different thing.
Put a definition in a guarded header:
/* counter.h */
#ifndef COUNTER_H
#define COUNTER_H
int counter = 0; /* a DEFINITION, not a declaration */
#endif
Include it once in each of two source files — not twice in one, just once each — and link them:
/usr/bin/ld: /tmp/ccq1kYCs.o:(.bss+0x0): multiple definition of `counter';
/tmp/cc45UbJV.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
The guard did its job perfectly in both translation units. Each one included the header exactly once. The problem is that “exactly once per file” times two files equals two definitions of counter in the finished program, and the linker is the first thing to see them together. No preprocessor directive can fix that, because the preprocessor has finished before the linker starts.
The fix is in the header’s contents, not its guard:
/* counter.h */
#ifndef COUNTER_H
#define COUNTER_H
extern int counter; /* declaration: says it exists somewhere */
#endif
/* counter.c */
#include "counter.h"
int counter = 0; /* definition: exactly one, in one .c file */
The rule for headers: put things there that are meant to be included in many files, and keep ordinary object definitions out. Function prototypes, struct and union definitions, typedefs, enums and macros are all fine in a header because they are declarations or type definitions rather than objects. Variables need extern in the header and a definition in exactly one source file. Templates, inline functions and inline variables (C++17), constexpr entities and C’s static inline functions are all designed to live in headers: the language permits them to be defined in every translation unit that includes them, provided the definitions are identical, and the implementation makes those definitions refer to one entity. Ordinary variables and non-inline functions have no such permission, which is what the linker error above is telling you.
#pragma once vs Include Guards
#pragma once is a non-standard directive that every mainstream compiler supports. One line at the top of the header replaces the three-line guard:
/* counter.h */
#pragma once
struct Counter { int value; };
void counter_bump(struct Counter *c);
Two claims usually settle this argument. I tested both, and neither survived.
Myth 1: #pragma once compiles faster
The reasoning goes that a guarded header must be opened and re-read every time, while #pragma once lets the compiler skip it. In practice GCC has had a multiple-include optimisation for decades: when it sees a header whose entire body is wrapped in a guard, it remembers the macro and skips the file on later inclusions.
How this was measured
| Setting | Value |
|---|---|
| Date tested | September 2026 |
| Machine | Single-core Linux VM, Ubuntu 24.04 |
| Compiler | GCC 13.3, -std=c17, no optimisation flags |
| Workload | 300 leaf headers, 60 mid-level headers each including 30 leaves, one .c including all 60 — 1,800 include edges, acyclic |
| Iterations | 3 warm-up runs discarded, then mean of 10 |
| Checked | Both variants exit 0; identical header contents apart from the guard style |
| Not captured | No median, no percentiles, no CPU-frequency control, no file-open counts |
| Style | Mean compile time |
|---|---|
| Include guards | 14 ms |
#pragma once | 15 ms |
Three runs of ten: guards 14, 14, 14 ms; #pragma once 15, 16, 15 ms. One millisecond in fourteen, on a graph with 1,800 include edges — that is noise, not a reason to choose. Note what this measures: total compile time, not file opens or preprocessing operations. A compiler may still stat a header it has decided not to reparse, and none of that was counted. If your build is slow, the fix is including fewer headers, not changing how you deduplicate the ones you include.
I should be straight about the first version of this benchmark, because it produced a dramatic result and the result was wrong. I built 200 headers that each included all the others, which looks like a stress test and is actually a cycle: the #pragma once variant blew past GCC’s maximum include depth and spent its time emitting thousands of errors. I had redirected stderr and never checked the exit status, so it looked like #pragma once was 2× slower. It was failing. The numbers above come from an acyclic graph where both variants compile successfully.
Myth 2: #pragma once breaks when a header is reachable by two paths
The usual warning is that #pragma once identifies files by path, so the same header reached through a symlink, a hard link or a copy gets included twice. I tested a hard link and a byte-identical copy, with a struct definition in the header so any double inclusion would be a compile error:
| Same header reached via | GCC 13.3 | MSVC 19.44 |
|---|---|---|
| Hard link (same inode) | Deduplicated | Included twice |
| Symlink | Deduplicated | Included twice |
| Byte-identical copy | Deduplicated | Included twice |
GCC deduplicated every case, including the copy, which has a different inode and a different path. MSVC deduplicated none of them, failing with a struct redefinition each time:
C:\guardtest\hard.h(2): error C2011: 'Tag': 'struct' type redefinition
C:\guardtest\real.h(2): note: see declaration of 'Tag'
So the folklore is real — it just depends which compiler you ask. GCC compares more than the path; MSVC, on this version, appears not to. Neither behaviour is required by any standard, because #pragma once is not in any standard. That is the whole argument in one table: a guard’s behaviour is defined by the macro and identical everywhere, while #pragma once is whatever your compiler decided to implement.
So which should you use?
| Property | Include guards | #pragma once |
|---|---|---|
| In the standard | Yes, C and C++ | No — universally supported, but an extension |
| Lines of boilerplate | Three | One |
| Can be typo’d or collide | Yes — see below | No name to collide |
| Behaviour across odd filesystems | Defined by the macro, always | GCC deduplicates aliased paths; MSVC 19.44 did not |
| Compile speed here | 14 ms | 15 ms |
Guards are portable by definition and carry a naming hazard. #pragma once removes the naming hazard and relies on a behaviour no standard requires. Both are defensible; pick one and be consistent, because a codebase using both gets the drawbacks of each. If you need to satisfy a strict standards-conformance requirement, guards are the only answer.
Guard Names: The Silent Failure
A guard name that collides with another header’s is the one failure mode in this whole topic that does not announce itself. Two headers, both using UTILS_H:
/* utils.h */ /* net/utils.h */
#ifndef UTILS_H #ifndef UTILS_H
#define UTILS_H #define UTILS_H
int from_utils(void); int from_net_utils(void);
#endif #endif
#include "utils.h"
#include "net/utils.h" /* body skipped: UTILS_H is already defined */
The second header is silently discarded. Under C17 you get a warning that is easy to lose in a busy build:
main.c:6:5: warning: implicit declaration of function 'from_net_utils';
did you mean 'from_utils'? [-Wimplicit-function-declaration]
Under C23 and in C++ the same code is a hard error, which is a real improvement:
main.c:6:5: error: implicit declaration of function 'from_net_utils'
But notice what the diagnostic says. It complains about the function, not about the header being skipped. Nothing anywhere says “your guard names collided”, and if the second header had only defined types rather than declared functions, you would have got no diagnostic at all until something failed much later.
Name guards after the project and the path, not just the file: MYPROJ_NET_UTILS_H. Some teams append a random suffix or a UUID, which is ugly and completely reliable.
One rule the compiler will not enforce for you: identifiers beginning with an underscore followed by a capital, and any identifier containing a double underscore, are reserved to the implementation. _UTILS_H and UTILS__H are both reserved, both extremely common in the wild, and both compile without a murmur:
(no diagnostic: the compiler does not police this)
These spellings are reserved to the implementation, which is free to use them for its own purposes. Your program should not define them even where the compiler accepts them without complaint. Start guard names with a letter.
#ifndef Beyond Include Guards
Include guards are the main use but not the only one. The other common pattern is a default that a build can override:
#include <stdio.h>
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 4096
#endif
int main(void) {
printf("Buffer size is %d bytes.\n", BUFFER_SIZE);
return 0;
}
Buffer size is 4096 bytes.
Compile with -DBUFFER_SIZE=8192 and the #ifndef sees the macro already defined, skips its body, and the command-line value wins. This is how build systems configure code without editing it.
C23 adds #elifdef and #elifndef, which spell #elif defined(X) more directly. Check your compiler’s flag name before reaching for them — the mode is spelled differently across GCC releases:
| Compiler and flag | Result |
|---|---|
GCC 13.3, -std=c2x | Accepted, prints cpu |
GCC 13.3, -std=c23 | error: unrecognized command-line option '-std=c23' |
GCC 14.2, -std=c2x | Accepted, prints cpu |
GCC 14.2, -std=c23 | Accepted, prints cpu |
Be more careful still about mixing them into older code. Compiled with -std=c17, the same program printed neither — it silently took the #else branch instead of the #elifdef one, with zero diagnostics on both compilers even under -Wall -Wextra -Wpedantic. An unrecognised conditional directive does not fail loudly; it changes which branch you compile. The full directive list is on cppreference’s conditional inclusion page.
Where C++20 Modules Leave All This
Modules are the long-term answer to the problem guards exist to patch. import std; or import mylib; gives the compiler a pre-parsed representation instead of pasting text, so there is nothing to deduplicate, no macro leakage across the boundary, and no include order to get wrong.
They are genuinely better, and they are also not yet a default you can assume. Support across GCC, Clang and MSVC has been landing unevenly for years, build-system integration is the harder half of the problem, and the standard library module has arrived more recently than the language feature. Most C++ written in 2026 still uses headers, and all C does — C has no standardised module system, so if you write C, include guards are not a transitional technology. They are the answer.
The practical position: keep writing guards, write them correctly, and treat modules as something to adopt when your compiler, your build system and your dependencies all agree rather than as a reason to defer learning the thing you will use this week.
Key Takeaways
- An include guard prevents a header being processed twice in one translation unit. That is its entire job, and it does it reliably.
- Guards do not prevent multiple definition across source files. A variable defined in a guarded header still produces multiple definition at link time. Declare with
externin the header, define once in a.cfile. - Keep ordinary object definitions out of headers. Prototypes, types,
typedefs,enums, macros, templates andinlineorstatic inlinefunctions all belong there. A plain variable does not. #pragma onceis not measurably faster. On a 1,800-edge include graph here, 15 ms against 14 ms for guards. Choose on portability and naming, not speed.- Guard name collisions fail silently. The second header is skipped and the diagnostic, if any, blames something else. Name guards after the project and path.
- Never start a guard name with an underscore or include a double underscore — those identifiers are reserved, and no compiler will warn you.
- C++20 modules offer a different model; C has no standardised equivalent. Headers and modules will coexist for years, so guards remain working knowledge in C++ and the permanent answer in C.
Frequently Asked Questions
Conclusion
The include guard is three lines of boilerplate that almost every C and C++ programmer types without thinking, and the thinking is worth doing once. Its scope is one translation unit. It protects against text being pasted twice, and against nothing else. Everything that goes wrong around headers — the linker complaining about a variable, the header that mysteriously has no effect, the macro that leaks into a file that never asked for it — comes from expecting it to do more.
Keep ordinary definitions out of headers and most of those problems never start. The rest of our C programming guides take the same approach, and if you are working through the language systematically, the complete guide to C++ programming covers where the header model is heading.
What I Could Not Verify
Everything here was compiled on one machine: a single-core Ubuntu 24.04 VM running GCC 13.3 and GCC 14.2 with libstdc++ and glibc 2.39. GCC and MSVC were tested; Clang was not. The #pragma once path results differ between the two compilers tested, which is itself the finding, but a third implementation could differ again. rather than guaranteed. The #pragma once deduplication of hard links and copies is GCC behaviour, not a standard requirement — Clang and MSVC use their own heuristics and may differ, which is itself the argument for guards where conformance matters. The compile-time comparison is one workload on one machine with cold caches unmanaged; the millisecond figures will not transfer, and the finding that the gap is negligible is an expectation for ordinary include graphs rather than something these three runs establish for all of them. I did not count file opens to confirm the mechanism behind that result, so the explanation of GCC’s multiple-include optimisation is documented behaviour I am citing, not something I measured. The statement that C++20 module support is still maturing is a general characterisation of the ecosystem rather than a test of any particular toolchain.



