📝 Lesson 14: Header Files and Forward Declarations
Learn how C++ separates declaration from implementation — and how include guards, forward declarations, and precompiled headers keep large projects (and Unreal builds) compiling fast.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Understand the C++ compilation model and the separation of declaration from implementation.
- Structure header files properly using include guards (#pragma once).
- Apply forward declarations to break circular dependencies and reduce compilation times.
- Implement effective include optimization strategies (Include-What-You-Use).
- Understand the role and usage of Precompiled Headers (PCH) for build optimization.
Estimated Time: 60–75 minutes
Project: Refactor a header-heavy project to improve compilation times using forward declarations.
In This Lesson
Understanding the Compilation Model
Think of C++ compilation like building a house. Header files (.h) are the blueprints that describe what rooms exist, while source files (.cpp) contain the actual construction details. This separation is crucial for large projects like games.
Header File Structure
A well-structured header file is like a contract - it tells other code what's available without revealing implementation details.
Complete Header Example
// Player.h - Header file example
#pragma once // Modern include guard (or use #ifndef/#define/#endif)
// Forward declarations instead of includes when possible
class AWeapon; // Just need to know it exists
class UHealthComponent;
struct FDamageInfo;
// Only include what you must
#include "CoreMinimal.h" // Unreal's core types
#include "GameFramework/Character.h" // Parent class
#include "Player.generated.h" // Unreal's generated code
// Class declaration
class MYGAME_API APlayer : public ACharacter {
GENERATED_BODY() // Unreal macro
public:
// Constructor/Destructor
APlayer();
virtual ~APlayer();
// Public interface
void EquipWeapon(AWeapon* NewWeapon);
float GetHealthPercent() const;
// Inline functions (defined in header)
FORCEINLINE int32 GetLevel() const { return Level; }
FORCEINLINE bool IsAlive() const { return Health > 0.0f; }
protected:
// Protected members
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
private:
// Private data
UPROPERTY()
AWeapon* CurrentWeapon; // Forward declared type
UPROPERTY()
UHealthComponent* HealthComp;
float Health;
int32 Level;
// Private methods
void UpdateHealthBar();
};
// Traditional include guards (alternative to #pragma once)
/*
#ifndef PLAYER_H
#define PLAYER_H
// ... header content ...
#endif // PLAYER_H
*/
📏 The One Definition Rule (ODR)
Include guards exist because of the One Definition Rule: a class, inline function, or template may be defined only once within any single translation unit, and every definition of the same entity across the whole program must be identical. If a header without a guard is included twice (directly, and again through another header), the class gets defined twice in that translation unit and the compiler rejects it. A guard — #pragma once or the #ifndef/#define/#endif trio — turns the second inclusion into a no-op, keeping you on the right side of the ODR.
#pragma once is not part of the C++ standard, but every major compiler (MSVC, Clang, GCC — and therefore Unreal) supports it, and it is the convention in Unreal code. The traditional #ifndef guard is 100% portable and needs no compiler support; both are correct, so pick one and stay consistent within a project.
Source File Implementation
The source file contains the actual code - the construction work based on the blueprint.
// Player.cpp - Source file
#include "Player.h" // Always include own header first
// Now include headers only needed for implementation
#include "Weapon.h" // Full definition needed here
#include "HealthComponent.h"
#include "Engine/World.h"
#include "Components/CapsuleComponent.h"
// Constructor implementation
APlayer::APlayer() {
// Set defaults
Health = 100.0f;
Level = 1;
// Create components
HealthComp = CreateDefaultSubobject<UHealthComponent>(TEXT("HealthComp"));
}
// Destructor
APlayer::~APlayer() {
// Cleanup if needed (rare in Unreal due to GC)
}
// Method implementations
void APlayer::EquipWeapon(AWeapon* NewWeapon) {
// Implementation details hidden from header
if (CurrentWeapon) {
CurrentWeapon->OnUnequipped();
}
CurrentWeapon = NewWeapon;
if (CurrentWeapon) {
CurrentWeapon->OnEquipped(this);
}
}
float APlayer::GetHealthPercent() const {
return Health / 100.0f;
}
void APlayer::BeginPlay() {
Super::BeginPlay();
// Initialization code
UpdateHealthBar();
}
void APlayer::UpdateHealthBar() {
// Private method implementation
// UI update code here
}
Forward Declarations: Breaking Dependencies
Forward declarations are like business cards - they tell the compiler "this thing exists" without providing full details. This reduces compilation time and breaks circular dependencies.
When to Use Forward Declarations
// Forward declaration rules and examples
// 1. When you only need pointers or references
class AWeapon; // Forward declaration
class APlayer {
AWeapon* CurrentWeapon; // OK - pointer
void EquipWeapon(AWeapon* Weapon); // OK - parameter
AWeapon& GetWeapon(); // OK - reference return
// AWeapon DirectWeapon; // ERROR - needs full definition
};
// 2. Function declarations
class UDamageType; // Forward declaration
float CalculateDamage(AActor* Target, UDamageType* DamageType); // OK
// 3. Template forward declarations
template<typename T> class TArray; // Forward declare template
void ProcessArray(TArray<int>* Array); // OK - pointer to template
// 4. Breaking circular dependencies
// Weapon.h
class APlayer; // Forward declare
class AWeapon {
APlayer* Owner; // OK
};
// Player.h
class AWeapon; // Forward declare
class APlayer {
AWeapon* Weapon; // OK
};
// 5. Nested class forward declarations
class Outer {
class Inner; // Forward declare nested class
Inner* InnerPtr; // OK
};
// 6. When you CAN'T use forward declarations
class Base; // Forward declaration
// ERROR: Can't inherit from forward declared class
// class Derived : public Base {};
// ERROR: Can't use sizeof with forward declared type
// size_t size = sizeof(Base);
// ERROR: Can't call methods on forward declared type
// void UseBase(Base* b) { b->Method(); }
Include Optimization Strategies
Managing includes is like organizing your toolbox - keep only what you need at hand to work efficiently.
Best Practices Example
// GameMode.h - Optimized header
#pragma once
// Minimal includes - only what's absolutely necessary
#include "GameFramework/GameModeBase.h"
#include "GameMode.generated.h"
// Forward declarations for everything else
class APlayerController;
class APlayerState;
class APawn;
class ASpawnPoint;
class UGameInstance;
struct FGameStats;
template<typename T> class TSubclassOf;
// Use UCLASS macro
UCLASS()
class MYGAME_API AMyGameMode : public AGameModeBase {
GENERATED_BODY()
public:
AMyGameMode();
// Methods using forward declared types
void RespawnPlayer(APlayerController* PC);
void UpdateGameStats(const FGameStats& Stats);
// Template with forward declaration
void RegisterSpawnPoint(ASpawnPoint* Point);
protected:
// Unreal's TSubclassOf works with forward declarations
UPROPERTY(EditDefaultsOnly, Category = "Classes")
TSubclassOf<APawn> DefaultPawnClass;
// Pointers to forward declared types
UPROPERTY()
TArray<ASpawnPoint*> SpawnPoints;
private:
// Even private members can use forward declared types
APlayerController* LastSpawnedController;
FGameStats* CurrentStats;
};
Looking Forward: C++20 Modules
C++20 introduced modules (export module, import), the language's long-term answer to the costs of the #include model. A module is compiled once into a binary interface and imported directly, so there are no include guards, no header/source duplication, and no macros leaking across translation units — which is exactly the manual bookkeeping this lesson has been managing. C++23 added the importable standard-library module, so import std; can replace a stack of <vector>/<string> includes, and compiler and build-system support (MSVC, Clang, GCC) matured through 2023–2026.
For this course we keep headers primary: Unreal Engine's build system and reflection tooling are still header-based, so everything above is exactly how you write Unreal C++ today. Treat modules as the direction the wider ecosystem is heading, and a tool to reach for in standalone C++ projects on a modern toolchain.
Precompiled Headers (PCH)
Precompiled headers are like pre-assembled components - commonly used parts are compiled once and reused, speeding up build times significantly.
Using Precompiled Headers
// MyProjectPCH.h - Precompiled header
#pragma once
// Include expensive headers that rarely change
#include <vector>
#include <string>
#include <memory>
#include <algorithm>
#include <functional>
// Platform headers
#ifdef _WIN32
#include <Windows.h>
#endif
// Engine headers (in Unreal)
#include "CoreMinimal.h"
#include "Engine/Engine.h"
#include "GameFramework/Actor.h"
// DON'T include frequently changing headers!
// #include "MyGameTypes.h" // Bad - changes often
// Using PCH in your files
// MyClass.cpp
#include "MyProjectPCH.h" // Must be first include
#include "MyClass.h" // Then your header
// ... rest of includes ...
Common Header File Problems
Understanding common pitfalls helps you avoid hours of debugging compilation errors.
// Problem 1: Circular Dependencies
// Character.h
#pragma once
#include "Weapon.h" // Problem: Weapon.h includes Character.h!
class ACharacter {
AWeapon Weapon; // Requires full definition
};
// Solution: Use forward declaration and pointers
// Character.h
#pragma once
class AWeapon; // Forward declaration
class ACharacter {
AWeapon* Weapon; // Pointer doesn't need full definition
};
// Problem 2: Missing Include Guards
// Bad.h - Missing guards causes redefinition errors
class MyClass {
// ...
};
// Good.h - With include guards
#pragma once // or #ifndef/#define/#endif
class MyClass {
// ...
};
// Problem 3: Including in Header When CPP Would Suffice
// Bad: Widget.h
#pragma once
#include "ComplexSystem.h" // 1000+ lines included everywhere!
class Widget {
void ProcessData();
};
// Good: Widget.h
#pragma once
class ComplexSystem; // Forward declare
class Widget {
void ProcessData();
};
// Widget.cpp
#include "Widget.h"
#include "ComplexSystem.h" // Include only where needed
// Problem 4: Inline Function Dependencies
// Bad: Forces everyone to include "Math.h"
// GameObject.h
#pragma once
#include "Math.h" // Expensive include
class GameObject {
Vector3 Position;
public:
Vector3 GetPosition() { return Position; } // Inline needs Vector3
};
// Good: Move to CPP or forward declare
// GameObject.h
#pragma once
class GameObject {
class Vector3* Position; // Or use pimpl idiom
public:
Vector3 GetPosition(); // Not inline
};
The PIMPL Idiom (Pointer to Implementation)
When you want a header to leak zero implementation detail — no private members, no dependency includes — reach for PIMPL. The class holds a single pointer to a forward-declared implementation struct, and everything private lives in the .cpp file. Callers then recompile only when the public interface changes, not when internals do.
// Widget.h - a fully insulated interface
#pragma once
#include <memory>
class Widget {
public:
Widget();
~Widget(); // Declared here, defined in the .cpp
void Process();
private:
struct Impl; // Forward-declared implementation
std::unique_ptr<Impl> pImpl; // The only data member
};
// Widget.cpp - all the real detail lives here
#include "Widget.h"
#include "ComplexSystem.h" // Heavy includes stay out of the header
struct Widget::Impl {
ComplexSystem system;
int cachedResult = 0;
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // Defined where Impl is a complete type
void Widget::Process() { pImpl->cachedResult = pImpl->system.Run(); }
Key detail: the destructor must be declared in the header but defined in the .cpp (even as = default). std::unique_ptr needs the complete definition of Impl to destroy it, and that definition only exists inside the .cpp.
Unreal Engine Specific Header Rules
Unreal Engine has specific requirements for headers that you must follow.
// Unreal Engine header requirements
// 1. Generated header must be last
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
// Other includes...
#include "MyActor.generated.h" // ALWAYS LAST!
// 2. Module API macro
class MYMODULE_API AMyActor : public AActor {
// MYMODULE_API exports the class from the module
};
// 3. IWYU (Include What You Use) pragma
#include "GameFramework/Actor.h" // IWYU pragma: export
// 4. Forward declarations in Unreal
class AActor; // OK
class MYMODULE_API AMyActor; // Include API macro
// 5. Common Unreal forward declarations
class UWorld;
class UObject;
class AActor;
class APawn;
class AController;
class UActorComponent;
// 6. Unreal's precompiled header
// YourProject.h (PCH)
#pragma once
#include "CoreMinimal.h" // Includes most common types
// YourFile.cpp
#include "YourProject.h" // Include PCH first
#include "YourFile.h" // Then your header
Practice Exercise: Header Optimization
🏋️ Challenge: Optimize This Header File
The following header file has multiple issues. Fix them using forward declarations and proper organization:
// GameCharacter.h - FIX THIS FILE
#include <vector>
#include <string>
#include "Weapon.h"
#include "Inventory.h"
#include "CharacterStats.h"
#include "AnimationSystem.h"
#include "ParticleEffects.h"
#include "SoundManager.h"
#include "UIHealthBar.h"
class GameCharacter {
private:
std::string name;
Weapon currentWeapon;
Inventory inventory;
CharacterStats stats;
AnimationSystem* animator;
ParticleEffects effects;
SoundManager sounds;
UIHealthBar* healthBar;
public:
GameCharacter() {
animator = new AnimationSystem();
healthBar = new UIHealthBar();
}
void Attack() {
currentWeapon.Fire();
effects.PlayMuzzleFlash();
sounds.PlaySound("attack");
}
std::vector<Item> GetAllItems() {
return inventory.GetItems();
}
};
// Issues to fix:
// 1. Too many includes
// 2. STL types in interface
// 3. Implementation in header
// 4. No include guards
// 5. Missing forward declarations
// 6. Value members that could be pointers
Key Takeaways
- ✓ Always use include guards (#pragma once or #ifndef/#define/#endif)
- ✓ Forward declare when you only need pointers or references
- ✓ Include headers in .cpp files rather than .h when possible
- ✓ Keep inline functions simple to avoid forcing includes
- ✓ Use precompiled headers for common, rarely-changing includes
- ✓ In Unreal, generated.h must be the last include
- ✓ Minimize header dependencies for faster compilation
- ✓ Break circular dependencies with forward declarations
Remember: Every header you include affects compilation time. Think of headers as expensive imports - only include what you absolutely need!
🎯 Quick Quiz
Question 1: What is the primary purpose of an include guard such as #pragma once (or the traditional #ifndef/#define/#endif pattern) at the top of a header file?
Question 2: Given only a forward declaration class AWeapon; (no #include "Weapon.h"), which member declaration is legal inside another class?
Question 3: Character.h and Weapon.h each try to #include the other, causing a circular dependency compile error. What is the standard fix?
Summary
🎉 Key Takeaways
- ✓ Always use include guards (
#pragma onceor#ifndef/#define/#endif) - ✓ Forward declare when you only need pointers or references
- ✓ Include headers in .cpp files rather than .h when possible
- ✓ Keep inline functions simple to avoid forcing includes
- ✓ Use precompiled headers for common, rarely-changing includes
- ✓ In Unreal,
generated.hmust be the last include - ✓ Minimize header dependencies for faster compilation
- ✓ Break circular dependencies with forward declarations
📚 Additional Resources
- cppreference — The #include directive
- cppreference — Incomplete types (why forward declarations only allow pointers/references)
- isocpp.org — Include guards FAQ
🚀 What's Next?
Now that you can organize code across clean, forward-declared headers, next up in Lesson 15: Templates you'll write generic classes and functions that work with any type — a single implementation that can serve int, double, or any class you've built, without duplicating code.
🎉 Compilation model conquered!
Understanding headers, forward declarations, and precompiled headers puts you ahead of most working C++ developers — this is exactly the discipline that keeps large codebases, and Unreal projects, building fast.