C++ Header Files : Declarations, Include Guards, and What
Key takeaways
How C++ headers declare APIs while .cpp files define behavior: ODR-safe patterns, include guards, forward declarations, templates, inline functions, and a small logger example.
Introduction
C++ headers (.h, .hpp) carry declarations that describe APIs; source files provide definitions. Headers are how you modularize code and share types across translation units.
// math.h - Header (declaration)
#ifndef MATH_H
#define MATH_H
int add(int a, int b); // Declaration
#endif
// math.cpp - Source (definition)
#include "math.h"
int add(int a, int b) { // Definition
return a + b;
}
To understand why C++ splits code this way, it helps to know what #include actually does: the preprocessor pastes the header’s text into the including file before compilation. The compiler then compiles each .cpp file, now called a translation unit, completely on its own, knowing nothing about the other .cpp files. The header is how main.cpp learns that a function int add(int, int) exists somewhere, so it can compile a call to it; the linker later connects that call to the one compiled definition in math.o. Almost every header rule in this article follows from those two facts: headers are copied into many translation units, and the linker must end up with exactly one definition of each non-inline thing.
That model also explains the trade-offs. Because headers are re-parsed in every file that includes them, heavy headers make builds slow, and a change to one widely included header forces most of the project to recompile. C++20 modules (Section 10) were designed to replace this text-pasting model, but headers remain the norm in most existing codebases.
Declaration vs Definition
| Declaration | Definition | |
|---|---|---|
| Role | Introduces a name | Provides the actual implementation |
| Duplication | Allowed (multiple times) | One per program (ODR) |
| Example | int add(int, int); | int add(int a, int b) { return a+b; } |
| Location | Usually in headers | Usually in .cpp files |
One Definition Rule (ODR)
ODR: Each definition must be unique across the whole program, with specific exceptions:
- Inline functions (including member functions defined inside a class body, which are implicitly inline)
- Templates
- Inline variables (C++17+), which includes
static constexprdata members since they are implicitly inline - Class definitions (must be identical in all TUs)
Namespace-scope const and constexpr variables are a separate case: they have internal linkage by default, so each translation unit gets its own private copy. That is why constexpr double PI = 3.14...; in a header does not cause a linker error, and also why taking &PI in two files gives two different addresses.
The two halves of the ODR fail very differently. Defining a non-inline function or variable in two translation units is caught by the linker with a clear message such as multiple definition of 'add(int, int)' (GNU ld) or LNK2005: "int __cdecl add(int,int)" already defined in math.obj (MSVC). But the “must be identical” rule for inline functions and classes is not checked: if two .cpp files see different definitions of the same class, perhaps because a macro changed a member’s type between them, the program links fine and behaves unpredictably. That silent variant is one reason headers should not depend on macros defined differently by different includers.
Include Guards
Traditional include guards
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
class Widget {
int value_;
public:
Widget(int v);
int getValue() const;
};
#endif // WIDGET_H
#pragma once
// widget.h
#pragma once
class Widget {
int value_;
public:
Widget(int v);
int getValue() const;
};
Comparison:
| Feature | #ifndef guards | #pragma once |
|---|---|---|
| Standard | Yes (C++98+) | No (widely supported) |
| Portability | Every conforming compiler | GCC, Clang, MSVC, ICC and most others |
| Simplicity | Verbose | Concise |
| Speed | Same in practice (modern compilers detect the guard pattern) | Same in practice |
Include guards solve a specific problem: the same header being pasted twice into one translation unit, usually indirectly (a.h and b.h both include common.h, and main.cpp includes both). Without a guard, the second copy redefines the class and the compiler reports error: redefinition of 'class Widget'. Guards do nothing across translation units; they do not prevent the multiple-definition linker errors discussed later.
The practical risks of each approach are different. With #ifndef guards, the danger is name collisions: two headers that both use UTILS_H, often after copying one file to create another, and the second header is silently skipped, producing baffling “does not name a type” errors. Including the project and path in the macro (MYPROJ_NET_UTILS_H) avoids that. With #pragma once, the compiler has to decide whether two paths refer to the same file, and in unusual setups (the same header reachable through symlinks, hard links or network drives) that decision can go wrong. Both are fine choices for most projects; pick one per codebase and stay consistent.
What belongs in headers
✅ Safe in headers
// declarations.h
#pragma once
#include <vector> // needed for IntVector and Stack below
// 1. Forward declarations
class Window;
// 2. Type aliases
using IntVector = std::vector<int>;
// 3. Enumerations
enum class Color { Red, Green, Blue };
// 4. Class declarations
class Widget {
int value_;
public:
Widget(int v) : value_(v) {} // Inline definition OK
int getValue() const; // Declaration only
};
// 5. Inline functions
inline int square(int x) {
return x * x;
}
// 6. constexpr functions
constexpr int cube(int x) {
return x * x * x;
}
// 7. Templates (full definition required)
template<typename T>
class Stack {
std::vector<T> data_;
public:
void push(const T& item) { data_.push_back(item); }
T pop() { T val = data_.back(); data_.pop_back(); return val; }
};
// 8. Inline variables (C++17+)
inline int globalCounter = 0;
// 9. constexpr variables
constexpr double PI = 3.14159265359;
// 10. extern declarations
extern int externalVariable;
Everything in this list is either a pure declaration (items 1–4 and 10 introduce names without creating storage or code) or a definition the ODR explicitly allows to repeat (inline functions, constexpr functions, which are implicitly inline, templates, inline variables, and internal-linkage constants). The common thread is that including this header in fifty .cpp files still leaves the linker with nothing to complain about.
Two items deserve a warning. inline int globalCounter = 0; gives the whole program one shared variable, which is correct, but it is still a mutable global, with all the initialization-order and threading issues that implies. And Widget(int v) : value_(v) {} being defined in the class body makes it implicitly inline, which is convenient for one-liners but means any change to that body recompiles every file that includes the header. Put function bodies in the header when they are tiny and performance-sensitive, and in the .cpp otherwise.
❌ Avoid in headers
// bad_header.h
// ❌ Non-inline function definitions
int add(int a, int b) { // ODR violation if included in multiple .cpp files
return a + b;
}
// ❌ Non-inline global variables
int globalVar = 42; // Multiple definitions!
// ❌ using namespace in headers
using namespace std; // Pollutes all includers
// ❌ Implementation details
static int helperFunction() { // Each TU gets its own copy
return 42;
}
The first two are the classic linker errors. Each .cpp that includes bad_header.h compiles its own definition of add and globalVar, and the linker sees several. What makes this confusing for beginners is that it works fine as long as only one .cpp includes the header, then breaks the day a second file does. The fixes are to move the definition into a .cpp, or mark it inline.
using namespace std; in a header is not an error, but it is forced on every file that includes the header, directly or indirectly, and can create ambiguities that are very hard to trace: a project function named count, distance or data suddenly collides with the standard library, and the error appears in a file that never wrote the using directive. The static helper compiles and links, but each translation unit gets its own copy, so a static variable inside it would have a separate value per file, and unused copies can trigger -Wunused-function warnings. If a helper must live in a header, inline gives one shared definition; if it is private to one .cpp, put it there in an anonymous namespace.
Complete example: Calculator
calculator.h
#pragma once
class Calculator {
public:
// Inline member functions
int add(int a, int b) const {
return a + b;
}
// Declarations only
int multiply(int a, int b) const;
double divide(double a, double b) const;
// Static utility
static int square(int x);
};
// Free function declaration
int factorial(int n);
calculator.cpp
#include "calculator.h"
#include <stdexcept>
int Calculator::multiply(int a, int b) const {
return a * b;
}
double Calculator::divide(double a, double b) const {
if (b == 0.0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}
int Calculator::square(int x) {
return x * x;
}
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
main.cpp
#include "calculator.h"
#include <iostream>
int main() {
Calculator calc;
std::cout << "5 + 3 = " << calc.add(5, 3) << "\n";
std::cout << "5 * 3 = " << calc.multiply(5, 3) << "\n";
std::cout << "10 / 2 = " << calc.divide(10, 2) << "\n";
std::cout << "4² = " << Calculator::square(4) << "\n";
std::cout << "5! = " << factorial(5) << "\n";
}
The three files show the typical division of labour. calculator.h is what users of the class read: it has no #includes because it uses only built-in types, which keeps it cheap to include. add is defined in the class body, so it is implicitly inline and the compiler can inline it at each call site. multiply, divide, square and factorial are only declared, and their bodies live in calculator.cpp, which is also the only file that needs <stdexcept>. Changing the error handling in divide therefore recompiles one file, not every user of Calculator.
A frequent mistake with this layout is forgetting to compile or link calculator.cpp. Building only main.cpp compiles without errors, because the declarations are satisfied, and then fails at link time with undefined reference to 'Calculator::multiply(int, int) const'. The fix is in the build command or build system (g++ main.cpp calculator.cpp, or adding the file to the CMake target), not in the code. The same error appears when a definition’s signature does not exactly match the declaration, for example if the .cpp version forgets the trailing const; in that case the compiler usually reports a mismatch, but for free functions it silently creates a different overload.
Template example
Templates must have full definitions visible in headers:
stack.h
#pragma once
#include <vector>
#include <stdexcept>
template<typename T>
class Stack {
std::vector<T> data_;
public:
void push(const T& item) {
data_.push_back(item);
}
T pop() {
if (data_.empty()) {
throw std::runtime_error("Stack is empty");
}
T value = data_.back();
data_.pop_back();
return value;
}
bool empty() const {
return data_.empty();
}
size_t size() const {
return data_.size();
}
};
Usage
#include "stack.h"
#include <iostream>
int main() {
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
std::cout << intStack.pop() << "\n"; // 20
std::cout << intStack.pop() << "\n"; // 10
}
Templates live in headers because a template is not code yet; it is a recipe. The compiler generates Stack<int> only when it sees Stack<int> used, and at that moment it needs the full body. If you declare the template in stack.h and put the member bodies in stack.cpp, the file that uses Stack<int> sees only declarations, the .cpp never sees Stack<int>, and nobody generates the code. The result is a linker error like undefined reference to 'Stack<int>::push(int const&)', which puzzles people because the definition clearly exists.
There are two standard ways around this when header-only is undesirable. Explicit instantiation: keep the bodies in stack.cpp and add template class Stack<int>; there for each type you support, which works well for a closed set of types. Or split the implementation into a stack_impl.h / .tpp file that the header includes at the end, which keeps the declarations readable while still making the bodies visible. Header-only templates are the default because they work for any type, at the cost of compile time in every file that uses them.
Forward declarations
Break circular dependencies and reduce compile time:
window.h
#pragma once
// Forward declaration instead of #include "widget.h"
class Widget;
class Window {
Widget* widget_; // Pointer OK with forward declaration
public:
Window();
~Window();
void setWidget(Widget* w);
Widget* getWidget() const;
};
window.cpp
#include "window.h"
#include "widget.h" // Full definition needed here
Window::Window() : widget_(nullptr) {}
Window::~Window() {
// Can use Widget here because we included the full definition
}
void Window::setWidget(Widget* w) {
widget_ = w;
}
Widget* Window::getWidget() const {
return widget_;
}
When forward declaration works:
- Pointers or references to the type
- Function parameters/return types (declaration only) When full definition needed:
- Creating objects
- Accessing members
- Using sizeof
- Inheritance
The reason a pointer member works with only class Widget; is that Window needs to know its own size, and a pointer has the same size whatever it points to. Everything that needs Widget’s layout, its size, its members, its constructor or destructor, has to wait for the full definition, which window.cpp provides by including widget.h. The payoff is that files including window.h no longer depend on widget.h: changing a private member of Widget recompiles widget.cpp and window.cpp, not everything that uses Window.
The rule of thumb many teams follow is: forward declare types you own when a pointer or reference is enough, and include headers for types from other libraries. Forward declaring a third-party class is fragile, since the library may later make it a type alias or a template, and forward declaring anything in namespace std yourself is undefined behavior (use <iosfwd> for stream types). The companion article on forward declarations covers the unique_ptr destructor trap in detail.
Common problems and solutions
Problem 1: Multiple definition error
// ❌ bad.h
int globalVar = 42; // Defined in header!
// Every .cpp that includes this gets a copy
// Linker error: multiple definition of 'globalVar'
Solution:
// ✅ good.h
extern int globalVar; // Declaration
// good.cpp
int globalVar = 42; // Definition (once)
// Or C++17 inline variable
inline int globalVar = 42; // OK in header
Both solutions create one variable shared by the whole program; they differ in where the definition lives. extern in the header plus one definition in a .cpp works in every C++ version and keeps the initial value out of the header, so changing it recompiles one file. inline (C++17) keeps everything in the header, which is convenient for header-only libraries. A third option, static int globalVar = 42; in the header, also compiles, but gives each translation unit its own separate variable: incrementing it in one file is invisible to the others. When I have seen “the counter resets for no reason” bugs in older codebases, that per-file static copy is a frequent culprit.
Problem 2: Circular includes
// a.h
#include "b.h"
class A {
B* b_;
};
// b.h
#include "a.h"
class B {
A* a_;
};
// Circular dependency!
Solution:
// a.h
#pragma once
class B; // Forward declaration
class A {
B* b_;
};
// b.h
#pragma once
class A; // Forward declaration
class B {
A* a_;
};
Circular includes are a problem even with guards, because the guard makes the second inclusion empty. If main.cpp includes a.h first, a.h includes b.h, and b.h tries to include a.h again, the guard skips it, so class B { A* a_; }; is compiled before A has been declared, and you get error: 'A' does not name a type (GCC) or unknown type name 'A' (Clang). Which header fails depends on include order, so the error can appear or disappear when unrelated files are reordered. The forward-declaration fix works as long as each class only stores pointers or references to the other; if A needs a B by value, the design has to change, for example by extracting the shared part into a third header.
Problem 3: Include bloat
// ❌ Slow compilation
// widget.h
#include <vector>
#include <string>
#include <map>
#include <algorithm>
// ... 20 more headers
class Widget {
int value_; // Only uses int!
};
Solution:
// ✅ Minimal headers
// widget.h
#pragma once
class Widget {
int value_;
public:
Widget(int v);
int getValue() const;
};
// widget.cpp - Heavy includes here
#include "widget.h"
#include <vector>
#include <string>
// ... other headers
Unused includes cost more than they appear to. Standard headers such as <algorithm>, <map> or <regex> expand to tens of thousands of lines, and that work is repeated for every translation unit that includes your header, directly or through other headers. Worse, unnecessary includes create hidden dependencies: code elsewhere starts using std::map “for free” because your header happened to include it, and removing the include later breaks files you never touched. Tools such as include-what-you-use (IWYU) or clangd’s unused-include warnings find these automatically, and GCC and Clang’s -H flag prints the full include tree for a file, which is often eye-opening.
Best practices
Self-contained headers
Every header should compile on its own:
// widget.h
#pragma once
#include <string> // Don't rely on includers to provide this
class Widget {
std::string name_; // Uses std::string
public:
Widget(const std::string& name);
};
Test: Put your header first in the .cpp:
#include "widget.h" // If this fails, header isn't self-contained
#include <iostream>
// ... other includes
Include order
// widget.cpp
#include "widget.h" // 1. Own header first
#include <vector> // 2. C++ standard library
#include <string>
#include "util.h" // 3. Project headers
#include "helper.h"
Putting the file’s own header first is not just style; it is a free, automatic test. If widget.h forgot #include <string>, compiling widget.cpp fails immediately, instead of the header only working because some other header happened to be included before it. Many style guides (Google’s, LLVM’s) require this order for that reason. Some teams put project headers before system headers instead, to catch project headers that accidentally rely on system includes; either convention is fine as long as the own-header-first rule holds. Tools like clang-format can sort include blocks automatically so the order stays consistent.
Minimize dependencies
// ❌ Heavy header
#include <vector>
#include <map>
class Widget {
std::vector<int> data_; // Exposes std::vector in header
};
// ✅ Lighter with pimpl
#include <memory>
class Widget {
struct Impl;
std::unique_ptr<Impl> pimpl_;
public:
Widget();
~Widget(); // must be defined in widget.cpp, where Impl is complete
};
Pimpl (“pointer to implementation”) moves every private data member into a struct defined only in the .cpp. The header then exposes nothing about the implementation, so adding or changing private members never recompiles the users of Widget, and for shared libraries the class layout (its ABI) stays stable across versions. The costs are an extra heap allocation per object, a pointer indirection on every member access, and more boilerplate. Declaring ~Widget(); in the header and defining it in the .cpp is mandatory: if the compiler generates the destructor in the header, it tries to delete an incomplete Impl and fails with an error about sizeof applied to an incomplete type. Use pimpl for large, frequently changing classes or library interfaces, not for small value types.
Performance impact
Header hygiene mainly affects build time, not run time. How much each technique helps depends heavily on the project, so measure your own build rather than expecting fixed percentages. In rough order of typical impact:
| Technique | What it saves |
|---|---|
| Removing unnecessary includes | Parsing work in every translation unit that includes the header |
| Forward declarations / pimpl | Parsing work, plus fewer files recompiled when a class changes |
| Precompiled headers | Re-parsing large, stable headers (standard library, third-party) in each file |
| Unity (jumbo) builds | Repeated header parsing across many small .cpp files, at the cost of incremental builds |
| C++20 modules | Text re-parsing entirely, where compiler and build system support is mature |
Clang’s -ftime-trace produces a per-file breakdown of where compile time goes (open the JSON in a Chrome trace viewer), and tools such as ClangBuildAnalyzer aggregate it across a project to show which headers are the most expensive. In my experience a handful of headers, often a project-wide “common.h” that includes everything, account for a large share of the total, so fixing those first gives most of the benefit.
Modern alternatives
C++20 Modules
// math.ixx (module interface)
export module math;
export int add(int a, int b) {
return a + b;
}
// main.cpp
import math;
int main() {
int result = add(5, 3);
}
Benefits:
- No include guards needed
- Faster compilation
- Better encapsulation
- Order-independent
A module is compiled once into a binary interface that importers load, instead of being re-parsed as text by every file. Macros defined inside a module do not leak out, and only what is marked export is visible, which removes whole classes of header problems. The catch is tooling. The .ixx extension is an MSVC convention (GCC and Clang accept other extensions such as .cppm), modules need build-system support to compile files in dependency order (CMake supports them from 3.28 with Ninja or recent Visual Studio generators), and import std; for the standard library is only available from C++23 on recent toolchains. Mixing modules with existing headers is possible but adds its own rules. For new code on a modern toolchain, modules are worth trying; for most existing projects, the header practices above remain the day-to-day reality. See C++20 modules basics for a hands-on introduction.
The test for each line in a header
A header is included into every file that uses it, so every line in it is compiled many times and every change to it rebuilds all of those files. The test for each line is whether a file that includes the header needs it in order to compile. Declarations of the functions and classes you expose pass. Templates and inline functions pass because their definitions must be visible where they are used. Most #include lines in a header deserve a second look: if the header only mentions a type through a pointer or reference, a forward declaration is enough, and the full include can move to the .cpp file.
Two things fail the test and cause real bugs: a non-inline function or variable definition, which produces a “multiple definition” link error as soon as two source files include the header, and using namespace at file scope, which changes name lookup in every file that includes it.
Related Articles
- Include errors
- Header guards
- Include path
- Forward declaration
- C++20 modules
- One Definition Rule
- C++ Preprocessor Directives
Frequently Asked Questions (FAQ)
Q. Two headers include each other and I get “incomplete type” or “does not name a type” errors. How do I fix it?
A. Include guards stop the infinite recursion, but they also mean one header is processed before the other class has been declared. Break the cycle by forward declaring the other class in one header (class B;) and using only pointers or references to it there, then include the full header in the .cpp file that calls its members. A forward declaration is not enough when the header needs the class’s size or members, such as a by-value data member or an inline function body that uses it.