Skip to content
VLSI Mentor

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:

entityODR allows it in a header?consumer needs the body?so it goes
function declarationyes (declarations are unlimited)noheader
function definitionnonosource
inline functionyesyes, to inline itheader
class definitionyesyes, for size and layoutheader
member function bodyas inline if in-classnosource (or in-class for one-liners)
template declaration onlyyes—insufficient — see §2
template definitionyesyesheader
constexpr functionyes (implicitly inline)yes, to evaluate itheader
inline variable (C++17)yesyesheader
static variableyes, but one per TU—neither — see §4
non-inline variable definitionnonosource, declared extern in the header
enum definitionyesyesheader

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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); }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 here

That links. But it covers exactly the types you listed, and nothing else:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Undefined symbols for architecture arm64:
  "double clampTo<double>(double, double, double)", referenced from:
      _main in tpl4main.o

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 inline

A 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 below

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  one header-only consumer preprocesses to :   9009 lines
  one split consumer preprocesses to       :    212 lines

Forty-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 / splitrange
full build (8 consumers)1.72×1.18–2.12×
one consumer edited, rebuild just it3.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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clang++ -std=c++20 -fsyntax-only -Iinclude include/vm/bus.h

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
};
}
#endif

The 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 writingput itbecause
a function others calldeclaration in the header, definition in the sourcethe ODR forbids two definitions; callers need only the signature
a one-line accessorin-class in the headerimplicitly inline, and visible for optimisation
a function body in a headermark it inlineotherwise it is Chapter 24.1 §3's duplicate symbol
a templatedefinition in the headerthe consumer must instantiate it — §2
a template for 3 known types with a big bodydefinition + explicit instantiation in the source§2's option two; the linker enforces the closed set
a small integral constantconstexpr in the headerinternal linkage, usually folded to nothing
a large constant objectinline constexpr in the headerone object instead of one per consumer
a mutable globalinline in the header (C++17), or extern + one definitiona plain definition is a duplicate symbol
a shared singletona function-local static inside an inline functionChapter 24.1 §7
a helper private to one source filestatic or an anonymous namespace, in the sourceChapter 24.5 §4
a typedefinition in the header, one header per typeChapter 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 and constexpr functions are already inline.
  • Prefer inline constexpr for header constants, and never a plain non-const definition.
  • Never static at 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 namespace and 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, inline and 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 double call failing as the proof.
  • That inline is an ODR exception implemented as a weak external symbol, not an optimisation request.
  • That in-class member definitions and constexpr functions are implicitly inline.
  • That header-level const/constexpr have internal linkage, and inline constexpr (C++17) gives one shared object.
  • That static at 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 double call fails with its own undefined symbol.
  • inline is an ODR exception, not an optimisation hint. It permits a definition in every translation unit and emits a weak external symbol. In-class member bodies and constexpr functions are implicitly inline.
  • Header constants: const/constexpr have internal linkage (a copy per translation unit, usually folded away); inline constexpr gives one shared object; a plain non-const definition is a duplicate symbol; static is 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 namespace or 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.