Header Best Practices
By the end of this lesson you'll be able to write headers that compile cleanly anywhere: you'll guard them against double-inclusion, put the right things in the header versus the source file, slim dependencies with forward declarations, and avoid the linker errors that break the One Definition Rule.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
Think of a header ( .h ) as the menu in a restaurant and the source file ( .cpp ) as the kitchen . The menu declares what's available — "Pasta, £9" — so customers (other files) know what they can order, without showing the recipe. The kitchen holds the definition : the actual cooking steps. Many tables can read the same menu, but there's only ever one recipe for each dish — print the full recipe on every menu and the restaurant ends up with conflicting copies. That "one recipe" rule is exactly the One Definition Rule in C++: a thing may be declared in many files, but defined only once.
📊 Header vs Source — What Goes Where
Goes in the header (.h)
Goes in the source (.cpp)
Function declarations (prototypes)
Function definitions (the bodies)
Class / struct definitions
Out-of-class member function bodies
inline functions & templates
Non-inline helper functions
constexpr / const constants
Static / global variable definitions
#pragma once at the very top
Its own header included first
Rule of thumb: the header says what exists; the source says how it works. Templates and inline functions are the exception — they must live in the header because the compiler needs the full body everywhere they're used.
1. Stop Double-Inclusion with #pragma once
A header often gets #include d through several paths — main.cpp includes app.h , which includes config.h , and main.cpp includes config.h too. Without protection, config.h gets pasted into the same file twice and you get redefinition errors. The fix is an include guard : put #pragma once on the first line and the compiler reads each header at most once per file. The classic portable form is #ifndef NAME / #define NAME / #endif , but #pragma once is one line, can't be mistyped, and every modern compiler supports it.
🔎 The two forms of include guard
Both do the same job — they stop the body being included twice:
Pick one per header — you don't need both. Use a unique macro name (often the file path in CAPS) if you go with the #ifndef form.
2. Declarations in the Header, Definitions in the Source
A declaration says a name exists and what its type is (a function prototype, ending in ; ). A definition provides the body. Headers should hold declarations ; the matching .cpp holds the definitions . The exceptions that do belong in the header are things the compiler must see everywhere they're used: inline functions, templates , constexpr values, and member functions written inside the class body. Read this self-contained header, run it, and check the output against the comments.
Now compare it with a header that breaks the rules. The version below has no guard, defines a normal function in the header, and drags in a heavy include — each one a real-world bug. The comments show exactly how to fix each mistake.
Your turn. The header below should be guarded and should only declare its function. Fill in the three blanks marked ___ using the hints, then run it.
3. Forward Declarations Cut Dependencies
Every #include in a header is a dependency : change the included file and everything that includes your header recompiles too. You can avoid many of these. If your header only needs a pointer or reference to a type — Car* or Car& — you don't need its full definition; a forward declaration class Car; is enough. That breaks the include chain and speeds up builds. You only need the full #include where you store the type by value , call its members, take sizeof , or inherit from it.
Now you try. The header below stores only a pointer to Vehicle , so it shouldn't include the heavy vehicle.h — a forward declaration is enough. Fill in the blank:
🔎 Deep Dive: include what you use, and keep includes light
Include-what-you-use (IWYU): every file should directly include a header for each name it uses, and not rely on getting it "for free" through another header. If you use std::vector , write #include < vector > yourself — don't assume #include < iostream > will drag it in. Self-contained files don't break when an unrelated include is removed.
Compile speed: heavy standard headers like < iostream > , < map > , or < regex > pull in thousands of lines. Including them in a widely-used header multiplies that cost across the whole project. Prefer forward declarations and push heavy includes down into the .cpp files that actually need them.
Pro Tips
- 💡 Guard first, always: the very first line of every header is #pragma once . Make it muscle memory.
- 💡 Include your own header first in each .cpp . If it's missing an include, you find out immediately instead of by accident.
- 💡 Prefer forward declarations in headers: a header with fewer #include s recompiles far less of the project when it changes.
- 💡 Mark header helpers inline : if a small function body really must live in a header, inline keeps the One Definition Rule happy.
Common Errors (and the fix)
- "redefinition of 'struct Point'" (missing guard): a header without #pragma once got included twice into one file. Add #pragma once as the first line of every header.
- "multiple definition of 'square(int)'" (linker, ODR): a non- inline function was defined in a header included by more than one .cpp . Move the body to a .cpp (leave only the declaration), or mark it inline .
- "#include nested too deeply" / hang (circular include): a.h includes b.h which includes a.h . Break the cycle with a forward declaration — if a.h only needs a B* , write class B; instead of #include "b.h" .
- "invalid use of incomplete type 'Car'": you forward-declared Car but then used it by value, called a member, or took sizeof . Those need the full type — add #include "car.h" in that file.
- "'string' was not declared" in a header: the header uses std::string but doesn't include < string > — it only compiled where some other include happened to provide it. Include what you use: add #include < string > to the header itself.
📋 Quick Reference
Goal
Do this
Why
Avoid double-include
#pragma once
Stops redefinition
Function in header
int f(int);
Declaration only
Function in source
int f(int n) {...}
One definition (ODR)
Body must be in header
inline / template
Visible everywhere, safe
Only need a pointer
class Car;
Forward declare, no include
Use a name
#include < string >
Include what you use
First include in .cpp
#include "this.h"
Proves it's self-contained
Frequently Asked Questions
Mini-Challenge: Design a Clean Logger Header
No blanks this time — just a brief and an outline. Apply every rule from the lesson: guard the header, include what you use, declare in the header and define in the source. Build it, run it, and check your output against the example in the comments.
🎉 Lesson Complete
- ✅ Every header starts with #pragma once (or #ifndef guards) to stop double-inclusion
- ✅ Headers hold declarations ; .cpp files hold definitions
- ✅ inline functions, templates, and constexpr values belong in the header
- ✅ Forward-declare ( class Car; ) when you only need a pointer or reference
- ✅ Include what you use — keep every file self-contained, push heavy includes into .cpp
- ✅ The One Definition Rule : declare many times, define exactly once
- ✅ Next lesson: Unit Testing — prove your code works as you build it
Practice quiz
What problem does #pragma once solve?
- It speeds up the program at runtime
- It links libraries automatically
- It stops a header being pasted into the same file twice (double-inclusion)
- It marks a function inline
Answer: It stops a header being pasted into the same file twice (double-inclusion). #pragma once (or #ifndef include guards) ensures a header is parsed at most once per file, preventing redefinition errors.
What is the classic, fully portable form of an include guard?
- #ifndef NAME / #define NAME / #endif
- #pragma guard
- #once
- #protect NAME
Answer: #ifndef NAME / #define NAME / #endif. The #ifndef/#define/#endif trio is the portable standard guard; #pragma once is the one-line modern equivalent.
What is the difference between a declaration and a definition?
- They are the same thing
- A declaration provides the body; a definition only names it
- A declaration is only for variables, never functions
- A declaration says a name and type exist; a definition provides the body or storage
Answer: A declaration says a name and type exist; a definition provides the body or storage. A declaration (like int add(int,int);) names something; a definition gives its actual body or storage.
What generally belongs in a header (.h) versus the source (.cpp)?
- Definitions in the header, declarations in the source
- Declarations in the header, definitions in the source
- Everything in the header
- Only main() in the header
Answer: Declarations in the header, definitions in the source. Headers hold declarations; the matching .cpp holds the definitions — the header says what, the source says how.
Which of these CAN correctly live (defined) in a header?
- inline functions, templates, and constexpr values
- A normal non-inline function definition
- A global variable definition
- Nothing may ever be defined in a header
Answer: inline functions, templates, and constexpr values. inline functions, templates, and constexpr must be visible everywhere they're used, so they belong in the header.
Why does a non-inline function defined in a header included by two .cpp files cause a linker error?
- The header is too large
- Headers cannot contain functions
- The linker sees two identical definitions — a violation of the One Definition Rule
- The compiler runs out of memory
Answer: The linker sees two identical definitions — a violation of the One Definition Rule. Two .cpp files including the definition give two copies, triggering a 'multiple definition' ODR error. Mark it inline or move the body to a .cpp.
When is a forward declaration (class Car;) enough instead of including the full header?
- When you store the type by value
- When you only need a pointer or reference to the type (Car* or Car&)
- When you call the type's member functions
- When you inherit from the type
Answer: When you only need a pointer or reference to the type (Car* or Car&). A pointer or reference only needs the name; storing by value, calling members, sizeof, or inheriting needs the full type.
You must include the full header (not just forward-declare) when you:
- Only take a pointer to the type
- Only take a reference to the type
- Never use the type at all
- Store the type by value, call its members, inherit from it, or take sizeof
Answer: Store the type by value, call its members, inherit from it, or take sizeof. The compiler needs the type's real size and layout when you store it by value, use members, inherit, or take sizeof.
What does 'include what you use' (IWYU) mean?
- Include as few headers as possible regardless of what you use
- Every file directly includes the header for each name it uses, instead of relying on transitive includes
- Always include <iostream> in every file
- Only include headers in .cpp files, never in headers
Answer: Every file directly includes the header for each name it uses, instead of relying on transitive includes. IWYU keeps files self-contained: if you use std::vector, include <vector> yourself rather than hoping another header drags it in.
How can you break a circular include (a.h includes b.h includes a.h)?
- Add #pragma once to both files only
- Delete one of the headers
- Replace an #include with a forward declaration where only a pointer/reference is needed
- Include each header twice
Answer: Replace an #include with a forward declaration where only a pointer/reference is needed. If a.h only needs a B*, write class B; instead of #include "b.h" to break the cycle.
Continue this course
- Previous: CMake & Build Systems
- Next: Unit Testing