C++ Part I · Module 24 · C++ Build and Project Architecture
What Belongs in a Header and What Belongs in a Source File
This is usually taught as a convention — declarations in the header, definitions in the source — and the convention is right about two thirds of the time, which is exactly often enough to be misleading. Templates break it, because a template definition has to be visible wherever it is instantiated. Inline functions break it in the other direction, because they belong in the header by design. Constants break it in a way that changed in C++17. And a class definition is a definition that goes in the header, which contradicts the convention's own wording. So rather than a rule to memorise, this chapter derives the answer from two questions you can ask about any entity: does the one-definition rule permit it in more than one translation unit, and does a consumer need to see its body in order to compile. Those two questions settle every case, including the ones the convention gets wrong, and they explain the costs — because putting something in a header is never free, and section 5 measures what it costs the people who include it.
Intermediate18 min readC++Part IFundamentalsBuild
Part I · Module 24 · Chapter 24.2 · What Belongs in a Header and What Belongs in a Source File
1. The Two Questions That Decide Everything
A header's contents are copied into every translation unit that includes it, which Chapter 24.1 §1 established. So putting something in a header means every consumer compiles it, and that creates two constraints:
Question one: does the ODR permit this entity in many translation units? Chapter 24.1 §2's third part is the list — class types, enumerations, inline functions and variables, templates, constexpr functions. If the entity is not on that list, defining it in a header is a duplicate-symbol error.
Question two: does a consumer need to see the body to compile? A caller of a function needs the signature, not the body — the linker supplies the body later. But a user of a template needs the body, because the compiler has to generate code for the specific type at the point of use. Likewise a user of a class needs the full class definition to know its size and lay out its members.
Everything follows:
| entity | ODR allows it in a header? | consumer needs the body? | so it goes |
|---|---|---|---|
| function declaration | yes (declarations are unlimited) | no | header |
| function definition | no | no | source |
inline function | yes | yes, to inline it | header |
| class definition | yes | yes, for size and layout | header |
| member function body | as inline if in-class | no | source (or in-class for one-liners) |
| template declaration only | yes | — | insufficient — see §2 |
| template definition | yes | yes | header |
constexpr function | yes (implicitly inline) | yes, to evaluate it | header |
inline variable (C++17) | yes | yes | header |
static variable | yes, but one per TU | — | neither — see §4 |
non-inline variable definition | no | no | source, declared extern in the header |
enum definition | yes | yes | header |
The rest of the chapter is the three rows people get wrong.
2. Templates: the Row That Breaks the Convention
Declare a template in a header and define it in a source file, exactly as the convention says, and this happens:
// tpl.h
template <typename T> T clampTo(T v, T lo, T hi); // declared only
// tpl.cpp
template <typename T> T clampTo(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); }
// tplmain.cpp
#include "tpl.h"
int main() { return clampTo(5, 0, 3); }Undefined symbols for architecture arm64:
"int clampTo<int>(int, int, int)", referenced from:
_main in tplmain.o
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1 (use -v to see invocation)The missing symbol is clampTo<int>, not clampTo. That is the whole explanation: a template is not code, it is a recipe for code, and a function is only emitted when some translation unit instantiates it for a concrete type. tpl.cpp contains the recipe and never uses it, so it emits nothing. tplmain.cpp needs clampTo<int> and has only the declaration, so it emits a reference to a symbol nobody defined.
There are exactly two fixes.
One: put the definition in the header. The ODR permits it, the consumer can then instantiate it, and the linker deduplicates the result. This is what almost every template in the standard library does, and it is the default answer.
Two: explicitly instantiate the types you support, in the source file.
// tpl3.cpp
template <typename T> T clampTo(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); }
template int clampTo<int>(int, int, int); // emit this one hereThat links. But it covers exactly the types you listed, and nothing else:
Undefined symbols for architecture arm64:
"double clampTo<double>(double, double, double)", referenced from:
_main in tpl4main.oThe double call fails. This is a real technique with a real purpose — it keeps a large template body out of every consumer, and it gives you a closed set of supported types enforced by the linker — and it is the right choice when a template is instantiated for three known types and has an expensive body. Chapter 18.7 is the general treatment; the honest summary is that option two trades compile time for flexibility, and you should take it deliberately rather than by accident.
A third option worth knowing: the definition can live in a separate file conventionally named .ipp, .tpp or -inl.h, included at the bottom of the header. That is purely organisational — the text still ends up in every consumer — but it keeps the header readable, which matters for a template with a long body.
3. Inline: the Row That Breaks It the Other Way
An inline function belongs in the header, and the reason is not what the keyword suggests.
inline is not primarily an optimisation request. Compilers decide inlining for themselves and have for twenty years. What the keyword guarantees is the ODR exception: this function may be defined in every translation unit, the definitions must be identical, and the linker will keep one. Chapter 24.1 §5 measured the mechanism — the symbol is emitted as weak external in each object file.
So the rule is: if a function's body is in a header, it must be inline. Without the keyword you get Chapter 24.1 §3's duplicate-symbol error. Two spellings do it implicitly and are worth knowing:
class Bus {
public:
int width() const { return 32; } // defined IN-CLASS -> implicitly inline
Resp read(Addr a, std::uint32_t& out) const; // declared only -> defined in the .cpp
};
constexpr int kLanes = 4; // constexpr function/variable -> implicitly inline-ish
constexpr int lanesFor(int w) { return w / 8; } // implicitly inlineA member function defined inside the class body is implicitly inline. That is why a one-line accessor in a class is legal in a header with no keyword, and why moving that same body out of the class into the header without adding inline breaks the build.
The cost of inline is real and is paid at compile time, not run time: every consumer parses and codegens the body, and all but one copy is discarded. §5 measures it.
4. Constants, and What C++17 Changed
This row has the most history and the most confusion.
// in a header
const int kMaxCycles = 1000; // internal linkage -> one per TU, usually fine
constexpr int kLanes = 4; // same: constexpr implies const
inline constexpr int kWidth = 32; // C++17: ONE object, program-wide
extern const int kTimeout; // declaration; define it in exactly one .cpp
int verbose = 0; // DEFINITION -> duplicate symbol error
inline int verbose2 = 0; // C++17: one object, program-wide
static int counter = 0; // one per TU -- see belowThree things to take away.
A const or constexpr variable at namespace scope has internal linkage, so each translation unit gets its own copy. For a small integral constant that is harmless — the value is usually folded into the instructions and no object exists at all. For a large constexpr std::array it means one copy per consumer in the binary, which is why inline constexpr is the better default in C++17 and later.
A non-const variable in a header is a definition and produces Chapter 24.1 §3's duplicate-symbol error. The pre-C++17 fix is extern in the header plus one definition in a source file; the modern fix is inline.
static at namespace scope in a header is the one to avoid. Chapter 24.1 §5 measured it giving two objects where inline gives one, and Chapter 24.1 §6 measured the bug that follows when an inline function uses it: one surviving function bound to one translation unit's variable, and the other translation unit's copy never incremented at all. static in a header is almost never what the author meant.
The one legitimate use of static at file scope is in a source file, where it means "this helper is private to this translation unit" — which is a useful thing to say, and Chapter 24.5 §4 shows what it does to the symbol table.
5. What a Header Costs Its Consumers
Putting a definition in a header is not free, and the cost is borne by everyone who includes it. Two hundred non-trivial functions, eight consumers, each consumer calling two of them:
one header-only consumer preprocesses to : 9009 lines
one split consumer preprocesses to : 212 linesForty-two times the text, in every consumer. That figure is deterministic and reproduces exactly. The compile-time consequence, as a median over seven runs:
| header-only / split | range | |
|---|---|---|
| full build (8 consumers) | 1.72× | 1.18–2.12× |
| one consumer edited, rebuild just it | 3.30× | 2.11–4.02× |
The incremental figure is the one that matters day to day, because that is what you pay on every edit. And note the direction: header-only is worse for incremental builds, which is the opposite of the usual intuition that fewer files means a faster build.
Two honest caveats about these numbers. Wall-clock build times on this machine are load-dominated — a no-op make once measured slower than a four-translation-unit rebuild at a load average of 18 on 8 CPUs — which is why the ratios carry ranges and why the deterministic line counts are the figures to trust. And the first version of this experiment was wrong in the opposite direction: with one-line function bodies, both sides preprocessed to the same 412 lines and header-only measured as 0.67×, that is, faster. The experiment was measuring the extra translation unit on the split side and nothing else. It was rebuilt with non-trivial bodies before any figure was published.
The trade is therefore: a header-only library is trivial to consume — no build integration, no link step, no library to ship — and it taxes every consumer's compile time forever. For a small utility that is a good trade. For a 5,000-line component with a dozen consumers it is not, and Chapter 24.4 is the set of techniques for getting the cost back down.
6. A Header's Other Obligations
Placement is most of the chapter, but a header has four more duties that belong here.
It must compile on its own. A header that only works when included after some other header has an undeclared dependency, and the first consumer to include it in a different order gets a cascade of errors far from the cause. The test is one line per header in your build:
clang++ -std=c++20 -fsyntax-only -Iinclude include/vm/bus.hIt must include what it uses. If vm/bus.h names std::uint32_t, it includes <cstdint> — it does not rely on vm/types.h happening to pull it in. The failure mode of getting this wrong is that removing an unrelated include from an unrelated header breaks your file.
It must not include what it does not use. The symmetric duty, and the expensive one. Chapter 24.4 measures a single unnecessary include costing 87,964 preprocessed lines per consumer.
It must have an include guard. Chapter 24.3 is that subject, including the guard-name collision that silently empties a header.
Two things a header must not do: a using namespace directive at file scope, which imposes it on every consumer and cannot be undone, and a macro with a short name, which has no scope at all and will collide.
7. The Class, Which Is Both
A class definition is a definition, and it goes in the header — which looks like a contradiction of the convention and is not, because §1's two questions both say header: the ODR permits class definitions in many translation units, and a consumer needs the complete definition to know the type's size.
The split within the class is the interesting part:
// vm/bus.h
#ifndef VM_BUS_H
#define VM_BUS_H
#include "vm/types.h"
namespace vm {
class Bus {
public:
Resp read(Addr a, std::uint32_t& out) const; // declared -> body in bus.cpp
Resp write(Addr a, std::uint32_t v);
int width() const { return 32; } // in-class -> implicitly inline
private:
std::uint32_t regs_[16] = {}; // layout -- MUST be here
};
}
#endifThe private data members are in the header, and they have to be, because the size of the class depends on them. That is the awkward part of C++'s model: a consumer who only calls read() still recompiles when a private member is added, because its own stack frames and member offsets depend on the size. Chapter 24.4 §6 measures exactly that fan-out, and §5 of that chapter covers the pimpl idiom, which is the standard way of hiding the data at the cost of an indirection.
A one-line accessor in-class is right. It is implicitly inline, it is visible for optimisation, and the alternative — declaring it and defining it in the source — costs a function call the compiler cannot see across translation units unless you enable link-time optimisation.
Anything longer goes in the source file. Not because of a line count, but because a body in the header is a body every consumer compiles, and because changing it then rebuilds every consumer rather than one object file.
8. The Decisions, With Reasons
| you are writing | put it | because |
|---|---|---|
| a function others call | declaration in the header, definition in the source | the ODR forbids two definitions; callers need only the signature |
| a one-line accessor | in-class in the header | implicitly inline, and visible for optimisation |
| a function body in a header | mark it inline | otherwise it is Chapter 24.1 §3's duplicate symbol |
| a template | definition in the header | the consumer must instantiate it — §2 |
| a template for 3 known types with a big body | definition + explicit instantiation in the source | §2's option two; the linker enforces the closed set |
| a small integral constant | constexpr in the header | internal linkage, usually folded to nothing |
| a large constant object | inline constexpr in the header | one object instead of one per consumer |
| a mutable global | inline in the header (C++17), or extern + one definition | a plain definition is a duplicate symbol |
| a shared singleton | a function-local static inside an inline function | Chapter 24.1 §7 |
| a helper private to one source file | static or an anonymous namespace, in the source | Chapter 24.5 §4 |
| a type | definition in the header, one header per type | Chapter 24.1 §4's violation becomes unrepresentable |
9. What to Actually Do
- Ask the two questions — does the ODR permit it in many translation units, and does the consumer need the body — rather than recalling the convention.
- Put template definitions in the header. Measured: a declaration alone gives
Undefined symbols: "int clampTo<int>(int, int, int)". - Use explicit instantiation deliberately when the type set is small and closed, knowing it is the linker that enforces the set.
- Mark every header-level function body
inline, and remember in-class definitions andconstexprfunctions are already inline. - Prefer
inline constexprfor header constants, and never a plain non-constdefinition. - Never
staticat namespace scope in a header. Chapter 24.1 §6 measured the bug. - Keep private data in the header and long bodies out of it. The data is required; the bodies are a tax on every consumer.
- Make every header compile standalone, include what it uses, and include nothing else.
- No
using namespaceand no short macro names in a header. - Check that an experiment can tell its two arms apart before trusting its number — §5's first version measured header-only as faster.
10. Mistakes Worth Making Once
Declaring a template in the header and defining it in the source. The link error names clampTo<int>, which is the clue.
Adding a type to an explicitly instantiated template and not adding the instantiation. It compiles everywhere and fails to link in the one translation unit that used the new type.
A function body in a header without inline. Duplicate symbol.
Moving an in-class body out of the class, still in the header, without adding inline. The same error, and surprising because nothing about the body changed.
int verbose = 0; in a header. A definition.
static in a header to silence a duplicate-symbol error. It works and creates one object per translation unit.
A header that only compiles after another one. The next consumer to include it first pays for that.
using namespace std; in a header. Every consumer now has it and cannot refuse.
A header-only library for a 5,000-line component. Measured: 42× the preprocessed text in every consumer, and 3.3× the incremental compile.
11. Interview Relevance
The frozen curriculum marks this chapter High. The question is nearly always "why does a template have to be defined in the header?", and the answer that lands explains the mechanism rather than restating the rule: a template is a recipe, code is emitted only where it is instantiated, and a translation unit holding only the recipe emits nothing — so the missing symbol is clampTo<int>, not clampTo. Mentioning explicit instantiation as the alternative, and that it fixes exactly the types you list, shows you have used both.
A good follow-up is "what is inline for?". The strong answer is that it is an ODR exception rather than an optimisation hint — it permits the definition in every translation unit and makes the symbol weak so the linker keeps one — and that compilers have made their own inlining decisions for decades.
If constants come up, knowing that a header-level const or constexpr has internal linkage, and that inline constexpr in C++17 is what gives you a single shared object, is the detail that dates your knowledge correctly.
12. What You Should Be Able to Say Now
- The two questions that decide placement, and why the "declarations in headers" convention is a compression of them that fails on templates,
inlineand constants. - That a template definition belongs in the header because the consumer instantiates it, and that the link error names the instantiation —
clampTo<int>— which is the diagnostic clue. - That explicit instantiation is the alternative and covers only the listed types, with the
doublecall failing as the proof. - That
inlineis an ODR exception implemented as aweak externalsymbol, not an optimisation request. - That in-class member definitions and
constexprfunctions are implicitly inline. - That header-level
const/constexprhave internal linkage, andinline constexpr(C++17) gives one shared object. - That
staticat namespace scope in a header gives one object per translation unit and enables Chapter 24.1 §6's bug. - That a header costs every consumer: 9,009 preprocessed lines against 212, a 1.72× full build and a 3.30× incremental one.
- That a class's private data must be in the header because size depends on it, which is what makes Chapter 24.4's fan-out unavoidable without pimpl.
13. Exercises
Exercise 1 — the template. Reproduce §2's link error. Read the symbol name. Then fix it both ways, and with the explicit-instantiation version add a double call and confirm which one fails.
Exercise 2 — inline. Put a two-line function body in a header with no inline and include it from two translation units. Read the error. Add inline, then confirm with nm -m that the symbol became weak external.
Exercise 3 — constants. Put constexpr std::array<int, 1000> in a header, include it from three translation units, and count the copies in the binary with nm. Then make it inline constexpr and count again.
Exercise 4 — the cost. Build §5's experiment. First with one-line bodies, and observe that header-only measures faster; then with non-trivial bodies. Compare the preprocessed line counts in both versions and explain why the first experiment could not have answered the question.
Exercise 5 — standalone headers. Add a -fsyntax-only check for every header to your build and fix whatever it finds.
14. Summary
- Two questions decide placement: does the ODR permit this entity in more than one translation unit, and does a consumer need to see its body to compile? Every row of §1's table follows, including the three the usual convention gets wrong.
- Templates go in the header, because code is emitted only where it is instantiated. Measured: the link error names
int clampTo<int>(int, int, int)— the instantiation, not the template. - Explicit instantiation in a source file is the alternative, and it covers exactly the types listed; the
doublecall fails with its own undefined symbol. inlineis an ODR exception, not an optimisation hint. It permits a definition in every translation unit and emits aweak externalsymbol. In-class member bodies andconstexprfunctions are implicitly inline.- Header constants:
const/constexprhave internal linkage (a copy per translation unit, usually folded away);inline constexprgives one shared object; a plain non-constdefinition is a duplicate symbol;staticis the one to avoid. - A header taxes every consumer. Measured: 9,009 preprocessed lines against 212 — 42× — with a 1.72× (1.18–2.12) full build and a 3.30× (2.11–4.02) incremental rebuild.
- An experiment must be able to tell its arms apart. The first version of that measurement reported header-only as faster (0.67×) because both sides preprocessed to the same 412 lines; checking that the two arms actually differ is cheaper than interpreting the result.
- A header also owes its consumers: it compiles standalone, includes what it uses, includes nothing else, has a guard, and imposes no
using namespaceor short macros.
Chapter 24.3 takes the guard requirement and measures what each mechanism actually guarantees — including a collision that makes a header expand to nothing while blaming the file that used it.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
