C++ Header Guards: #ifndef vs #pragma once, Portability, and Modules
이 글의 핵심
Header guards stop duplicate inclusion of the same header. Learn traditional #ifndef guards, #pragma once trade-offs, naming conventions, circular dependency fixes, and PCH/module notes.
Introduction
You will see errors like:
error: redefinition of 'class MyClass'
Usually the same header was included twice in one translation unit. Header guards prevent duplicate processing of the same file.
Note: Guards dedupe the same include path. Different paths to duplicate copies can still bite—normalize include paths in build settings.
flowchart TD
A[Include header] --> B{Header guard present?}
B -->|No| C[Redefinition errors]
B -->|Yes| D{Already included?}
D -->|Yes| E[Skip contents]
D -->|No| F[Include contents]
F --> G[Define guard macro]
style C fill:#ff6b6b
style E fill:#51cf66
style F fill:#51cf66
What is a header guard?
A preprocessor technique that skips header contents on second inclusion within a TU.
#ifndef style (standard)
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass {
public:
MyClass(int v);
int getValue() const;
private:
int value;
};
#endif // MYCLASS_H
Pros: portable, explicit. Cons: boilerplate; macro name collisions if too generic.
#pragma once
#pragma once
class MyClass { /* ... */ };
Pros: one line; no macro name. Cons: not ISO C++ (but supported by major compilers); edge cases with duplicate paths via symlinks.
How #ifndef works (conceptually)
First include: macro undefined → process body and define macro. Second include: macro defined → skip.
How #pragma once works (conceptually)
Compiler remembers file identity (path/inode) and skips reprocessing.
Practical patterns
- Basic header +
.cppsplit - Diamond includes: multiple headers include
point.h—guardedpoint.hprocesses once - Namespaces: guard
math_utils.happropriately - Templates: still need guards even though definitions are in headers
Naming conventions
Prefer project-unique macro names, e.g. MYPROJECT_SRC_FOO_BAR_BAZ_H_ (see Google style), not generic UTILS_H.
Choosing between #pragma once and #ifndef
| Situation | Common choice |
|---|---|
| Library (max portability) | #ifndef |
| App (modern toolchain) | #pragma once |
| Embedded/exotic compilers | #ifndef |
Some teams use both defensively.
Circular includes
Problem: a.h includes b.h includes a.h — sometimes incomplete types break.
Fixes:
- Forward declare when only pointers/references are needed
- Interface split (abstract base)
- Dependency inversion (shared abstraction)
Performance
Forward declarations and fewer includes reduce compile time more than micro-differences between guard styles.
PCH
Precompiled headers bundle stable includes (<vector>, <memory>, project common headers) to speed builds.
C++20 modules (contrast)
Modules avoid textual inclusion and macro leakage; migration is incremental.
Inline functions and templates
Even with guards, non-inline function definitions in headers can still cause ODR violations across TUs. inline functions and templates follow the usual ODR exceptions.
“Perfect header” template (sketch)
Include guard, system headers sorted, forward declarations, namespaces, class declarations, small inline helpers—end guard.
Checklists
- Every header has a guard
- Macro names are unique
#endifcomment matches the opening guard
FAQ (highlights)
Covers #pragma once vs #ifndef, redefinition symptoms, circular dependency strategies, naming, whether every header needs guards (yes for classic headers; modules differ), forward declaration limits, modules vs guards, compile-time impact, tooling (clang-tidy llvm-header-guard).
Quick decision (header authoring)
flowchart TD
A[Write a header] --> B{Project type?}
B -->|New project| C{C++20 modules viable?}
B -->|Legacy| D{Existing style?}
B -->|Open-source library| E[Use #ifndef]
C -->|Yes| F[Consider modules]
C -->|No| G[#pragma once common]
D -->|#ifndef| E
D -->|#pragma once| G
D -->|Mixed| H[Stay consistent]
E --> I[PROJ_PATH_FILE_H]
G --> J[#pragma once]
F --> K[export module]
style F fill:#4dabf7
style G fill:#51cf66
style E fill:#ffd43b
Compile error triage
flowchart TD
A[Compile error] --> B{Error kind?}
B -->|redefinition| C[Check header guards]
B -->|does not name a type| D[Check circular includes]
B -->|undefined reference| E[Check definitions / link]
C --> C1{Guard present?}
C1 -->|No| C2[Add guard]
C1 -->|Yes| C3[Check macro name collisions]
D --> D1[Add forward declarations]
D1 --> D2[Prefer pointers / references]
E --> E1[Add definitions in .cpp]
E1 --> E2[Check linker inputs]
Related posts (internal links)
- C++20 modules
- Forward declaration
- Include path
Practical tips
Debugging
- Warnings; include graph tools.
Performance
- Trim includes; consider PCH.
Code review
- Guard present? Unique name?
Practical checklist
Before coding
- Header self-contained?
While coding
- Guard present?
During review
- No duplicate definitions?
Keywords (search)
C++, header guard, preprocessor, include, modules
Related posts
- Header files
- Include path
- Preprocessor tricks
- include errors