C++ Header Guards: #ifndef vs #pragma once, Portability, and Modules

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 + .cpp split
  • Diamond includes: multiple headers include point.h—guarded point.h processes once
  • Namespaces: guard math_utils.h appropriately
  • 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

SituationCommon 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:

  1. Forward declare when only pointers/references are needed
  2. Interface split (abstract base)
  3. 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
  • #endif comment 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]

  • 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?

C++, header guard, preprocessor, include, modules


  • Header files
  • Include path
  • Preprocessor tricks
  • include errors