C++ Module 4 – Functions, files and scope
C++ header files and the one-definition rule
Split C++ code into headers and source files, stop a header from being read twice, and fix the duplicate and undefined symbol errors of the linker.
What you will learn
- Split a program into a header and source files, and say what belongs in each
- Prevent a header from being pasted twice into one file with an include guard or #pragma once
- Fix duplicate-symbol and undefined-symbol link errors, templates included
- Use extern or an inline variable to share one variable or constant between files
- Recognise one-definition-rule breaks that build without any error
Before you start
What you’ll learn · Before you start
What you will learn
- Split a program into a header and source files, and say what belongs in each
- Prevent a header from being pasted twice into one file with an include guard or #pragma once
- Fix duplicate-symbol and undefined-symbol link errors, templates included
- Use extern or an inline variable to share one variable or constant between files
- Recognise one-definition-rule breaks that build without any error
Before you start
On this page
Put int orders_seen = 0; in a header that two .cpp files include, and both files compile while the link fails
with one duplicate symbol: A name in an object file's table, such as a function's mangled name. nm marks a symbol the file defines in its code with T, and one it uses but leaves to another file with U. More in “How C++ compiles: preprocess, compile and link”. Move a template’s body from its header into a .cpp file, and the link fails again, this
time with two undefined symbols. Both failures come from the rule that governs how a C++ program is split into files:
the one-definition rule: The rule that a program holds exactly one definition of each ordinary function it uses. An inline function may be defined in every translation unit that uses it, as long as all the definitions are identical. More in “How C++ compiles: preprocess, compile and link”.
You will split a program into a header and two source files, stop a header from being read twice into one file, fix both link errors, choose how to share a variable or a constant between files, and watch two breaks of the rule build without any error and print a wrong answer. Apple clang 21 built every program, and GCC 16 most of them.
A header says what exists; one source file says how it works
A C++ program of any size is split into headers (.h or .hpp files) and source files (.cpp files). A
header says what exists: the declarations of functions, the definitions of types and a few constants. A source file
says how it works: the bodies of the functions. Every source file that needs a function includes its header, and the
compiler translates each source file on its own; the linker: The last stage of a build: it joins the object files of a program and the libraries it uses into one executable, matching every symbol a file uses with exactly one definition. More in “How C++ compiles: preprocess, compile and link” then joins the results into one
program.
#include "grades.h" tells the preprocessor to paste the whole text of grades.h in place of that line. The header
is not compiled on its own: it becomes part of every translation unit: One source file after preprocessing: the .cpp file with the text of every header it includes, minus the lines that #if removed. The compiler turns each one into one object file, without seeing the others. More in “How C++ compiles: preprocess, compile and link” that includes
it. So a header holds what every one of those files needs to see, and nothing that would then exist once per file:
| Criterion | In a header | In exactly one .cpp file |
|---|---|---|
| Functions | declarations: char grade(int marks); | definitions: the body of grade() |
| Variables | extern declarations, or inline variables (C++17) | the one definition of each non-inline variable |
| Types | struct, class and enum definitions that other files use | types used by that file only |
| Inline and constexpr functions, templates | the whole definition, body included | only those that no other file uses |
| When to choose | Anything a second .cpp file must see | Anything that must exist once in the program |
The split program of this lesson’s practice section looks like this once it is built. Each .cpp file becomes one
translation unit, with its own pasted copy of the header:
One header, two translation units, one definition of each function
Text description of the diagram
The diagram follows the split grades program from top to bottom.
- The header grades.h declares the functions grade() and average() and defines the type Student and the constant pass_mark.
- The preprocessor pastes the text of grades.h into grades.cpp and, separately, into main.cpp. That makes two translation units, and each one is compiled on its own.
- Compiling the first gives grades.o, which defines grade(int) and average(...): nm marks them T. Compiling the second gives main.o, which only uses them: nm marks them U, for undefined.
- The linker matches every U in main.o with exactly one T in grades.o and writes the executable, split.
If no object file defines a function that is used, the link fails with an undefined symbol. If two object files define it, the link fails with a duplicate symbol, unless the function is inline.
nm lists the symbols of an object file: The output of compiling one translation unit, a .o file (.obj with MSVC): machine code plus a table of the symbols the file defines and the symbols it still needs from other files. More in “How C++ compiles: preprocess, compile and link”. In grades.o the
functions are marked T, defined in the code section; in main.o they are marked U, used but not defined there.
The linker’s job is to find exactly one T for every U.
The two spellings of #include differ in where the file is looked for. For #include "file", GCC and Clang look
first in the folder of the file that contains the #include, then in the folders of the -I options, then in the
system folders; for #include <file>, they skip the first step. Use quotes for your own headers and angle brackets
for the standard library and other installed libraries.
Checkpoint
Check: What goes in a header
An include guard or #pragma once stops a second paste
A header can reach one translation unit twice without anyone writing it twice. Here report.cpp includes two
headers, and both of them include money.h:
C header (money.h) · diamond/money.h
// money.h: a header without an include guard.
struct Money {
long paise;
}; C header (invoice.h) · diamond/invoice.h
// invoice.h
#include "money.h"
Money invoice_total(); C header (refund.h) · diamond/refund.h
// refund.h
#include "money.h"
Money refund_total(); C++ · diamond/report.cpp
// report.cpp: includes two headers, and both of them include money.h.
#include "invoice.h"
#include "refund.h"
int main() {} from session import run
# report.cpp reaches money.h twice: once through invoice.h, once through refund.h.
run("""
clang++ -std=c++23 -c diamond/report.cpp -o report.o
g++-16 -std=c++23 -c diamond/report.cpp -o report.o
""") Output
$ clang++ -std=c++23 -c diamond/report.cpp -o report.o
In file included from diamond/report.cpp:3:
In file included from diamond/refund.h:2:
diamond/money.h:2:8: error: redefinition of 'Money'
2 | struct Money {
| ^
diamond/invoice.h:2:10: note: 'diamond/money.h' included multiple times, additional include site here
2 | #include "money.h"
| ^
diamond/refund.h:2:10: note: 'diamond/money.h' included multiple times, additional include site here
2 | #include "money.h"
| ^
diamond/money.h:2:8: note: unguarded header; consider using #ifdef guards or #pragma once
2 | struct Money {
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c diamond/report.cpp -o report.o
In file included from diamond/refund.h:2,
from diamond/report.cpp:3:
diamond/money.h:2:8: error: redefinition of 'struct Money'
2 | struct Money {
| ^~~~~
In file included from diamond/invoice.h:2,
from diamond/report.cpp:2:
diamond/money.h:2:8: note: previous definition of 'struct Money'
2 | struct Money {
| ^~~~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 diamond_session.py
The translation unit now contains the definition of Money twice, which the one-definition rule forbids inside a
single translation unit, and both compilers stop with “redefinition”. Clang’s last note even names the two usual
fixes. The first is an include guard: The lines #ifndef NAME and #define NAME at the top of a header, with #endif at its end, so that a second #include of the header in the same translation unit is skipped. It protects one translation unit, not the whole program.: the whole header sits inside #ifndef and
#endif, and the first inclusion defines the guard macro, so a second inclusion finds it defined and skips
everything:
// money.h, now with an include guard: the second #include of this file in a
// translation unit finds MONEY_H defined and skips everything up to #endif.
#ifndef MONEY_H
#define MONEY_H
struct Money {
long paise;
};
#endif // MONEY_H from session import run
# The same three headers, with an include guard in money.h.
run("""
clang++ -std=c++23 -Wall -Wextra -c diamond_fixed/report.cpp -o report.o && echo compiled
g++-16 -std=c++23 -Wall -Wextra -c diamond_fixed/report.cpp -o report.o && echo compiled
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c diamond_fixed/report.cpp -o report.o && echo compiled compiled $ g++-16 -std=c++23 -Wall -Wextra -c diamond_fixed/report.cpp -o report.o && echo compiled compiled
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 diamond_fixed_session.py
Step through both builds of report.cpp and count how many times the text of money.h reaches the translation
unit:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Line 2 of report.cpp. #include "invoice.h" is replaced by the whole text of invoice.h, and the preprocessor reads that text before it goes on with report.cpp.
- money.h, the first time. The first line of invoice.h includes money.h, so the text of money.h comes first: the definition struct Money { long paise; }.
- Line 3 of report.cpp. After the declaration of invoice_total() comes the second line of report.cpp, #include "refund.h". The first line of refund.h includes money.h again.
- No guard: pasted twice. Nothing in money.h records that it has been read, so its text is pasted a second time. The translation unit now holds two definitions of Money.
- Both compilers stop. Two definitions of one type in one translation unit break the one-definition rule. Clang reports a redefinition of Money, GCC a redefinition of struct Money, and neither writes report.o.
- With the guard. Now money.h starts with #ifndef MONEY_H and #define MONEY_H. The first inclusion defines MONEY_H and pastes Money; the second finds MONEY_H defined and skips everything up to #endif.
- One unit at a time. Both compilers now print compiled. The guard protects this translation unit only. Every other .cpp file starts with no macros defined and pastes its own copy of money.h.
Without the guard the text arrives twice; with it, the second #include is skipped and the unit holds one Money.
The second fix is the single line #pragma once at the top of the header. It is not part of the C++ standard, but
GCC, Clang and Microsoft’s compiler all support it. The two vendors’ manuals recommend opposite things: GCC’s
recommends the #ifndef wrapper, because not every preprocessor recognises #pragma once, while Microsoft’s
recommends #pragma once for new code, because it cannot collide with another header’s guard macro. Either is fine
in a project that builds only with those compilers; what matters is that every header has one of them.
If you write guards, make the macro unique, built from the file name, such as MONEY_H or SHOP_MONEY_H. Do not
start it with an underscore followed by a capital letter, or put two underscores in a row anywhere in it: the
standard reserves such names for the compiler and its library. GCC and Clang both recognise the guard pattern and skip
a guarded header that is included again without reading it a second time, so guards cost nothing.
Expecting a guard to protect the whole program
A guard is a macro, and every .cpp file is preprocessed separately, starting with no macros defined. So each
translation unit still gets its own copy of the header, and a definition in the header is made once per file. That
is exactly what the next section is about.
Checkpoint
Check: Include guards and pragma once
A program needs exactly one definition of each function and variable
The standard’s rule, in plain words, has three parts:
- Inside one translation unit, nothing may be defined twice. The diamond above broke this part, and the compiler reported it.
- In the whole program, every function and every variable that is used needs exactly one definition, unless it
is
inline. None is an “undefined symbol” error from the linker; two is a “duplicate symbol” error. - Types,
inlinefunctions,inlinevariables and templates may be defined in many translation units, and usually are, because they live in headers. Every one of those definitions must be identical: the same sequence of tokens, with every name meaning the same thing.
Outside C++20 modules, the standard requires no diagnostic for parts 2 and 3. A compiler sees one translation unit at a time, so it cannot compare files; what you get depends on what the linker happens to check.
A variable defined in a header
This header has a guard, but its variable line is a definition, not a declaration:
C header · orders/orders.h
// orders.h: the guard is fine, but the variable line is a definition, not a declaration.
#ifndef ORDERS_H
#define ORDERS_H
int orders_seen = 0; // every .cpp file that includes this header defines orders_seen
void take_order();
#endif // ORDERS_H C++ (take.cpp) · orders/take.cpp
// take.cpp
#include "orders.h"
void take_order() {
++orders_seen;
} C++ (main.cpp) · orders/main.cpp
// main.cpp
#include <iostream>
#include "orders.h"
int main() {
take_order();
take_order();
std::cout << "orders seen: " << orders_seen << '\n';
} from session import run
# Each file compiles on its own; the problem appears when the linker sees both object files.
run("""
clang++ -std=c++23 -c orders/take.cpp -o take.o
clang++ -std=c++23 -c orders/main.cpp -o main.o
clang++ take.o main.o -o orders_demo
g++-16 -std=c++23 -c orders/take.cpp -o take.o
g++-16 -std=c++23 -c orders/main.cpp -o main.o
g++-16 take.o main.o -o orders_demo
""") Output
$ clang++ -std=c++23 -c orders/take.cpp -o take.o
$ clang++ -std=c++23 -c orders/main.cpp -o main.o
$ clang++ take.o main.o -o orders_demo
duplicate symbol '_orders_seen' in:
take.o
main.o
ld: 1 duplicate symbols
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
[exit status 1]
$ g++-16 -std=c++23 -c orders/take.cpp -o take.o
$ g++-16 -std=c++23 -c orders/main.cpp -o main.o
$ g++-16 take.o main.o -o orders_demo
duplicate symbol '_orders_seen' in:
take.o
main.o
ld: 1 duplicate symbols
collect2: error: ld returned 1 exit status
[exit status 1]
This output changes from run to run: the linker may list the two object files in either order
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 orders_session.py
Both object files define orders_seen, and the linker refuses to choose. (Apple’s linker shows the name as the
object file stores it, _orders_seen: macOS adds a leading underscore to every symbol name.) There are two clean
fixes. Either the header only declares the variable with extern int orders_seen; and exactly one .cpp file defines
it, or, since C++17, the header defines it as an inline variable: A variable declared inline, as in inline int orders_seen = 0; (C++17). It may be defined in every translation unit that includes its header, and the linker keeps one object for the whole program.,
inline int orders_seen = 0;, which the rule allows in every translation unit and which the linker merges into one
object:
Fix 1: orders.h · orders_fixed/orders.h
// orders.h, fixed: the header only declares the variable.
#ifndef ORDERS_H
#define ORDERS_H
extern int orders_seen; // a declaration: "this variable is defined in some .cpp file"
void take_order();
#endif // ORDERS_H Fix 1: take.cpp · orders_fixed/take.cpp
// take.cpp, fixed: the one definition of orders_seen lives here.
#include "orders.h"
int orders_seen = 0;
void take_order() {
++orders_seen;
} Fix 2: orders.h · orders_inline/orders.h
// orders.h, the C++17 alternative: an inline variable may be defined in a header.
// Every translation unit that includes it refers to one and the same object.
#ifndef ORDERS_H
#define ORDERS_H
inline int orders_seen = 0;
void take_order();
#endif // ORDERS_H from session import run
# Fix 1: extern in the header and one definition in take.cpp.
# Fix 2: an inline variable defined in the header.
run("""
clang++ -std=c++23 -Wall -Wextra orders_fixed/take.cpp orders_fixed/main.cpp -o fix1 && ./fix1
g++-16 -std=c++23 -Wall -Wextra orders_fixed/take.cpp orders_fixed/main.cpp -o fix1 && ./fix1
clang++ -std=c++23 -Wall -Wextra orders_inline/take.cpp orders_inline/main.cpp -o fix2 && ./fix2
g++-16 -std=c++23 -Wall -Wextra orders_inline/take.cpp orders_inline/main.cpp -o fix2 && ./fix2
""") Output
$ clang++ -std=c++23 -Wall -Wextra orders_fixed/take.cpp orders_fixed/main.cpp -o fix1 && ./fix1 orders seen: 2 $ g++-16 -std=c++23 -Wall -Wextra orders_fixed/take.cpp orders_fixed/main.cpp -o fix1 && ./fix1 orders seen: 2 $ clang++ -std=c++23 -Wall -Wextra orders_inline/take.cpp orders_inline/main.cpp -o fix2 && ./fix2 orders seen: 2 $ g++-16 -std=c++23 -Wall -Wextra orders_inline/take.cpp orders_inline/main.cpp -o fix2 && ./fix2 orders seen: 2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 orders_fixed_session.py
Step through the failing link and both fixes, and watch what each object file holds for orders_seen:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- orders.h is pasted twice. Both files include orders.h, and its line int orders_seen = 0; is a definition. The guard does not help, because each file is preprocessed on its own.
- Each file compiles. Each translation unit is correct on its own, so both compile without a word. take.o and main.o each hold a definition of orders_seen.
- The linker refuses. The linker must give every name one definition and finds two. Apple's linker prints duplicate symbol _orders_seen in take.o and main.o, then 1 duplicate symbols.
- Fix 1: extern. orders.h now says extern int orders_seen;, which only declares the variable, and take.cpp holds its one definition. main.o just uses the name.
- One definition, one use. The linker joins the use in main.o to the definition in take.o. Both calls of take_order() add to the same variable, and the program prints orders seen: 2.
- Fix 2: an inline variable. With inline int orders_seen = 0; in the header, both object files define the variable again, which the rule allows for an inline variable. The linker keeps one object, and both compilers print orders seen: 2.
The failing build had two definitions; fix 1 has one definition and one use, and fix 2 has two inline copies that the
linker merges. In every working build, both calls of take_order() counted on the same variable.
Key idea
A header that many files include may declare an ordinary variable or function, but may define only what the rule allows in every file: types, inline functions and variables, and templates.
Checkpoint
Check: One definition per program
A const in a header is one copy per file; an inline variable is one object
Constants in headers need the same care. A const variable at namespace scope has
internal linkage: What a name has when no other translation unit can refer to it: a static variable or function at namespace scope, a const variable there that is not inline or extern, or anything in an unnamed namespace. Each file gets its own private copy. More in “Scope, storage duration, linkage and namespaces”: each translation unit that includes the header gets its own
private copy, which is harmless for a small number and wasteful or wrong for anything whose address or identity
matters. An inline variable is one object for the whole program:
// settings.h: two settings for the whole program, written two ways.
#ifndef SETTINGS_H
#define SETTINGS_H
// An inline variable (C++17): one object, shared by every file that includes this header.
inline int log_level = 1;
// A const variable at namespace scope has internal linkage:
// every .cpp file that includes this header gets a private copy of it.
const int max_retries = 3;
const int* log_level_in_network();
const int* max_retries_in_network();
#endif // SETTINGS_H C++ (network.cpp) · rates/network.cpp
// network.cpp: reports the addresses of the two variables as this file sees them.
#include "settings.h"
const int* log_level_in_network() { return &log_level; }
const int* max_retries_in_network() { return &max_retries; } C++ (main.cpp) · rates/main.cpp
// main.cpp: compares those addresses with the ones main.cpp sees.
#include <iostream>
#include "settings.h"
int main() {
std::cout << std::boolalpha;
std::cout << "log_level is one object: " << (&log_level == log_level_in_network()) << '\n';
std::cout << "max_retries is one object: " << (&max_retries == max_retries_in_network()) << '\n';
log_level = 3; // a change made here is seen by network.cpp too
std::cout << "network.cpp now sees log_level " << *log_level_in_network() << '\n';
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra rates/network.cpp rates/main.cpp -o settings_demo && ./settings_demo
g++-16 -std=c++23 -Wall -Wextra rates/network.cpp rates/main.cpp -o settings_demo && ./settings_demo
clang++ -std=c++23 -c rates/network.cpp -o network.o && nm -m network.o | grep -E 'log_level|max_retries'
""") Output
$ clang++ -std=c++23 -Wall -Wextra rates/network.cpp rates/main.cpp -o settings_demo && ./settings_demo log_level is one object: true max_retries is one object: false network.cpp now sees log_level 3 $ g++-16 -std=c++23 -Wall -Wextra rates/network.cpp rates/main.cpp -o settings_demo && ./settings_demo log_level is one object: true max_retries is one object: false network.cpp now sees log_level 3 $ clang++ -std=c++23 -c rates/network.cpp -o network.o && nm -m network.o | grep -E 'log_level|max_retries' 0000000000000000 (__TEXT,__text) external __Z20log_level_in_networkv 000000000000000c (__TEXT,__text) external __Z22max_retries_in_networkv 000000000000001c (__TEXT,__const) non-external __ZL11max_retries 0000000000000018 (__DATA,__data) weak external _log_level
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rates_session.py
nm -m shows how the linker sees the two variables: log_level is a
A symbol that may be defined in several object files: the linker keeps one copy and drops the rest. Inline functions and inline variables are stored this way.
symbol, which tells the linker to keep one copy and drop the rest, while max_retries is non-external, a private
copy of this object file that no other file can see.
Several things are inline without the keyword: a member function defined inside its class, every constexpr
function, and every static constexpr data member of a class (since C++17). That is why a class can be defined
completely in a header. A constexpr variable at namespace scope, on the other hand, is implicitly const but not
inline, so it gets one copy per file unless you write inline constexpr.
Which declaration fits a value your header shares? Answer three questions about it, and the table marks the ways that fit, with the results of this lesson’s runs:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| In the header | Links in two files | One object | Can change at run time | Header holds it all |
|---|---|---|---|---|
| int x = 0; What each file gets: a definition of its own | Poor duplicate symbol _orders_seen: 1 duplicate symbols | Poor a definition in every file that includes it | Good an ordinary variable | Poor it is in the header, and that is the bug |
| extern int x; What each file gets: a declaration; one .cpp file defines x | Good fix 1 linked: orders seen: 2 | Good every use is joined to the definition in take.cpp | Good two calls of take_order() counted 2 | Poor one .cpp file must still define it |
| inline int x = 0; What each file gets: the same object, merged by the linker | Good fix 2 linked with both compilers: orders seen: 2 | Good log_level is one object: true | Good main.cpp set log_level to 3, and network.cpp saw 3 | Good needs no .cpp file (C++17) |
| const int x = 3; What each file gets: a private copy | Good internal linkage: each file may hold its copy | Poor max_retries is one object: false | Poor const: it keeps its first value | Good needs no .cpp file |
| constexpr int x = 3; What each file gets: a private copy of a constant | Good implicitly const, so internal linkage | Poor one copy per file unless written inline | Poor a constant | Good needs no .cpp file |
| inline constexpr int x = 40; What each file gets: one constant, shared | Good pass_mark in grades.h linked with both compilers | Good inline makes it one object | Poor a constant | Good needs no .cpp file |
How the answers decide
- “Must the value change while the program runs, as a counter does?” Yes, it changes: rules out int x = 0;. Every file that includes the header defines it again: the link of take.o and main.o failed with a duplicate symbol.
- “Must the value change while the program runs, as a counter does?” No, it is a constant: rules out int x = 0;. Every file that includes the header defines it again: the link of take.o and main.o failed with a duplicate symbol.
- “Must the value change while the program runs, as a counter does?” Yes, it changes: rules out const int x = 3;, constexpr int x = 3; and inline constexpr int x = 40;. A const or constexpr variable keeps its first value, so a counter such as orders_seen must be an ordinary variable.
- “Must the value change while the program runs, as a counter does?” No, it is a constant: adds a caveat to extern int x; and inline int x = 0;. A value that never changes reads better as a constant, and then the compiler stops a file from changing it by mistake.
- “Must every file see the same object, so that a change or an address in one file holds in all?” Yes, one object: rules out const int x = 3; and constexpr int x = 3;. A const or constexpr variable at namespace scope is a private copy in every file: max_retries had a different address in each.
- “Must every file see the same object, so that a change or an address in one file holds in all?” No, a copy per file will do: adds a caveat to const int x = 3; and constexpr int x = 3;. A copy per file is harmless for a small number such as 3, and wasteful for a large table.
- “Should the header hold the whole definition, with no .cpp file to own it?” Yes, the header alone: rules out extern int x;. extern only declares the variable: one .cpp file must still define it, as take.cpp does in fix 1.
- “Should the header hold the whole definition, with no .cpp file to own it?” No, one .cpp file can own it: adds a caveat to inline int x = 0; and inline constexpr int x = 40;. An inline variable also works with a .cpp file around; extern with one definition keeps the value out of the header.
In short: a counter or a setting that changes is an inline variable, or an extern declaration with one definition;
a constant shared as one object is inline constexpr; and a plain const is fine when a private copy per file does
no harm.
Checkpoint
Check: Sharing variables and constants
A template’s body must be visible where it is used
A template is not code yet: the compiler writes largest<int> only when a translation unit uses it, and it needs
the template’s body at that moment. This program keeps the body in a .cpp file:
C header · largest/largest.h
// largest.h: the template is declared here...
#ifndef LARGEST_H
#define LARGEST_H
#include <vector>
template <typename T>
T largest(const std::vector<T>& values);
#endif // LARGEST_H C++ (largest.cpp) · largest/largest.cpp
// largest.cpp: ...and defined here, where no file ever asks for largest<int>.
#include "largest.h"
template <typename T>
T largest(const std::vector<T>& values) {
T best = values.front();
for (const T& v : values) {
if (best < v) best = v;
}
return best;
} C++ (main.cpp) · largest/main.cpp
// main.cpp: uses largest<int> and largest<double>.
#include <iostream>
#include "largest.h"
int main() {
std::cout << largest(std::vector<int>{3, 9, 4}) << '\n';
std::cout << largest(std::vector<double>{2.5, 7.25, 1.5}) << '\n';
} from session import run
# largest.o ends up with no code at all: nothing in largest.cpp asked for an instantiation.
run("""
clang++ -std=c++23 -c largest/largest.cpp -o largest.o
clang++ -std=c++23 -c largest/main.cpp -o main.o
nm -C largest.o
clang++ largest.o main.o -o largest_demo
""") Output
$ clang++ -std=c++23 -c largest/largest.cpp -o largest.o
$ clang++ -std=c++23 -c largest/main.cpp -o main.o
$ nm -C largest.o
0000000000000000 t ltmp0
$ clang++ largest.o main.o -o largest_demo
Undefined symbols for architecture arm64:
"double largest<double>(std::__1::vector<double, std::__1::allocator<double>> const&)", referenced from:
_main in main.o
"int largest<int>(std::__1::vector<int, std::__1::allocator<int>> const&)", referenced from:
_main in main.o
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 largest_session.py
nm finds nothing in largest.o but an internal label: no file asked largest.cpp for largest<int>, so it made
none. main.cpp asked, but it could see only the declaration, so it left a U for the linker that nothing matches.
The usual fix is to put the whole template in the header. The other fix keeps the body in the .cpp file and adds
explicit instantiation: A line such as template int largest(const std::vector<int>&); in the file that holds a template's body. That file then writes the code of the named instantiation, so other files can link to it without seeing the body. for every type the program needs, which suits a
library that supports a fixed list of types and wants to compile the body once:
// largest.cpp, fixed the other way: the definition stays here, and the two
// explicit instantiations at the end make this file emit largest<int> and largest<double>.
#include "largest.h"
template <typename T>
T largest(const std::vector<T>& values) {
T best = values.front();
for (const T& v : values) {
if (best < v) best = v;
}
return best;
}
template int largest(const std::vector<int>&);
template double largest(const std::vector<double>&); from session import run
# Fix 1: the template in the header. Fix 2: explicit instantiations in largest.cpp.
run("""
clang++ -std=c++23 -Wall -Wextra largest_fixed/main.cpp -o fix1 && ./fix1
clang++ -std=c++23 -c largest_explicit/largest.cpp -o largest.o
nm -C largest.o | grep ' largest<'
clang++ -std=c++23 -c largest_explicit/main.cpp -o main.o
clang++ largest.o main.o -o fix2 && ./fix2
""") Output
$ clang++ -std=c++23 -Wall -Wextra largest_fixed/main.cpp -o fix1 && ./fix1 9 7.25 $ clang++ -std=c++23 -c largest_explicit/largest.cpp -o largest.o $ nm -C largest.o | grep ' largest<' 00000000000001c0 T double largest<double>(std::__1::vector<double, std::__1::allocator<double>> const&) 0000000000000000 T int largest<int>(std::__1::vector<int, std::__1::allocator<int>> const&) $ clang++ -std=c++23 -c largest_explicit/main.cpp -o main.o $ clang++ largest.o main.o -o fix2 && ./fix2 9 7.25
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 largest_fixed_session.py
Step through the failing build and the two fixes, and watch which file can see the body when largest<int> is
needed:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A body that nobody asks for. largest.cpp holds the whole template, but nothing in that file calls largest, so the compiler writes no code from it. nm finds only an internal label, ltmp0.
- Calls without a body. main.cpp calls largest on a vector of int and a vector of double. It sees only the declaration from largest.h, so it compiles two calls and leaves two undefined symbols.
- Two undefined symbols. The linker looks for a definition of each instantiation and finds none, so it reports Undefined symbols for largest<double> and largest<int>, referenced from main.
- Fix 1: the body in the header. With the whole template in largest.h, main.cpp can see the body where it calls largest, and instantiates largest<int> and largest<double> itself. The program prints 9 and 7.25.
- Fix 2: explicit instantiation. The body stays in largest.cpp, followed by two lines such as template int largest(const std::vector<int>&);. Now nm lists T largest<double> and T largest<int> in largest.o, and the link works.
- Copies are allowed. A function template may be instantiated in every translation unit that uses it. The linker merges the identical copies, so the header version never gives a duplicate symbol.
A function template, unlike an ordinary function, may be defined in every translation unit that uses it, and the linker merges the identical copies, so the header version never gives a duplicate symbol.
Checkpoint
Check: Templates and their bodies
Two different definitions of one name can build without an error
The linker catches two definitions of an ordinary function or variable. It cannot catch two different
definitions of something the rule allows to be defined many times, because it expects many copies and simply keeps
one. Here two files each define an inline function called unit(), with different bodies:
C++ (scale.cpp) · units/scale.cpp
// scale.cpp: this team's helper reports weights in kilograms.
inline const char* unit() { return "kg"; }
const char* unit_in_scale() { return unit(); } C++ (main.cpp) · units/main.cpp
// main.cpp: another team wrote a helper with the same name that reports pounds.
// Two different definitions of one inline function break the one-definition rule,
// and nothing is required to tell you.
#include <iostream>
inline const char* unit() { return "lb"; }
const char* unit_in_scale();
int main() {
std::cout << "scale.cpp says " << unit_in_scale() << ", main.cpp says " << unit() << '\n';
} from session import run
# The same two files, built four ways. No build prints a warning.
run("""
clang++ -std=c++23 -Wall -Wextra -O0 units/scale.cpp units/main.cpp -o units_demo && ./units_demo
clang++ -std=c++23 -Wall -Wextra -O0 units/main.cpp units/scale.cpp -o units_demo && ./units_demo
clang++ -std=c++23 -Wall -Wextra -O2 units/scale.cpp units/main.cpp -o units_demo && ./units_demo
g++-16 -std=c++23 -Wall -Wextra -O0 units/main.cpp units/scale.cpp -o units_demo && ./units_demo
""") Output
$ clang++ -std=c++23 -Wall -Wextra -O0 units/scale.cpp units/main.cpp -o units_demo && ./units_demo scale.cpp says kg, main.cpp says kg $ clang++ -std=c++23 -Wall -Wextra -O0 units/main.cpp units/scale.cpp -o units_demo && ./units_demo scale.cpp says lb, main.cpp says lb $ clang++ -std=c++23 -Wall -Wextra -O2 units/scale.cpp units/main.cpp -o units_demo && ./units_demo scale.cpp says kg, main.cpp says lb $ g++-16 -std=c++23 -Wall -Wextra -O0 units/main.cpp units/scale.cpp -o units_demo && ./units_demo scale.cpp says lb, main.cpp says lb
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 units_session.py
No build warns. Without optimisation, both files call the function, and in these runs the linker kept the
definition from whichever object file came first on the command line, so the order of the files decided the answer.
With -O2, each file inlined its own definition and the call disappeared, so each file saw its own unit. The program
is wrong in every case, and the standard does not require any tool to say so.
Types break the same way, and more dangerously, when a header’s contents depend on a macro that differs between
files. Here item.cpp is built with -DWITH_LABEL and main.cpp without it, so the two files disagree about the
layout of Item:
C header · item/item.h
// item.h: the layout of Item depends on a macro that the build sets per file.
#ifndef ITEM_H
#define ITEM_H
struct Item {
int id;
#ifdef WITH_LABEL
char label[8];
#endif
int qty;
};
Item make_item();
#endif // ITEM_H C++ (item.cpp) · item/item.cpp
// item.cpp: built with -DWITH_LABEL, so here Item has three members.
#include "item.h"
Item make_item() {
return Item{7, {'p', 'e', 'n'}, 25};
} C++ (main.cpp) · item/main.cpp
// main.cpp: built without -DWITH_LABEL, so here Item has two members.
#include <iostream>
#include "item.h"
int main() {
Item it = make_item();
std::cout << "id " << it.id << ", qty " << it.qty << '\n';
} from session import run
# Two translation units disagree about Item. The ordinary build is silent;
# GCC's link-time optimisation compares the two definitions and reports them.
run("""
g++-16 -std=c++23 -Wall -Wextra -O2 -DWITH_LABEL -c item/item.cpp -o item.o
g++-16 -std=c++23 -Wall -Wextra -O2 -c item/main.cpp -o main.o
g++-16 item.o main.o -o item_demo && ./item_demo
clang++ -std=c++23 -O2 -flto -DWITH_LABEL -c item/item.cpp -o item.o
clang++ -std=c++23 -O2 -flto -c item/main.cpp -o main.o
clang++ -O2 -flto item.o main.o -o item_demo && ./item_demo
g++-16 -std=c++23 -O2 -flto -DWITH_LABEL -c item/item.cpp -o item.o
g++-16 -std=c++23 -O2 -flto -c item/main.cpp -o main.o
g++-16 -O2 -flto item.o main.o -o item_demo
""") Output
$ g++-16 -std=c++23 -Wall -Wextra -O2 -DWITH_LABEL -c item/item.cpp -o item.o
$ g++-16 -std=c++23 -Wall -Wextra -O2 -c item/main.cpp -o main.o
$ g++-16 item.o main.o -o item_demo && ./item_demo
id 7, qty 7234928
$ clang++ -std=c++23 -O2 -flto -DWITH_LABEL -c item/item.cpp -o item.o
$ clang++ -std=c++23 -O2 -flto -c item/main.cpp -o main.o
$ clang++ -O2 -flto item.o main.o -o item_demo && ./item_demo
id 7, qty 7234928
$ g++-16 -std=c++23 -O2 -flto -DWITH_LABEL -c item/item.cpp -o item.o
$ g++-16 -std=c++23 -O2 -flto -c item/main.cpp -o main.o
$ g++-16 -O2 -flto item.o main.o -o item_demo
item/item.h:5:8: warning: type 'struct Item' violates the C++ One Definition Rule [-Wodr]
5 | struct Item {
| ^
item/item.h:5:8: note: a different type is defined in another translation unit
5 | struct Item {
| ^
item/item.h:8:10: note: the first difference of corresponding definitions is field 'label'
8 | char label[8];
| ^
item/item.h:10:9: note: a field with different name is defined in another translation unit
10 | int qty;
| ^
item/item.h:13:6: warning: 'make_item' violates the C++ One Definition Rule [-Wodr]
13 | Item make_item();
| ^
item/item.cpp:4:6: note: return value type mismatch
4 | Item make_item() {
| ^
item/item.h:5:8: note: type 'struct Item' itself violates the C++ One Definition Rule
5 | struct Item {
| ^
item/item.cpp:4:6: note: 'make_item' was previously declared here
4 | Item make_item() {
| ^
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 item_session.py
Step through the two layouts and find where the wrong qty comes from:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- item.cpp writes an Item. item.cpp is compiled with -DWITH_LABEL, so its Item has three members in 16 bytes: id, an 8-byte label and qty. make_item() fills them with 7, "pen" and 25.
- main.cpp's Item. main.cpp is compiled without the macro. The same header gives it an Item of two members in 8 bytes, with qty right after id.
- id is right. it.id reads bytes 0 to 3 in both views, so main.cpp prints id 7, as it should.
- qty is not. it.qty reads bytes 4 to 7, where item.cpp put the start of the label: p, e, n and a zero byte, 0x70 0x65 0x6E 0x00. Read as an int, lowest byte first, that is 0x6E6570, which is 7234928.
- Nothing complains. The real qty, 25, sits in bytes 12 to 15, which main.cpp's Item does not have. The ordinary build and Clang's -flto build print no warning; GCC's -flto build reports -Wodr for Item and the first field that differs, label.
main.cpp reads qty where item.cpp wrote the label. The value 7234928 is 0x6E6570 in hexadecimal, and its bytes,
read from the lowest, are 0x70, 0x65 and 0x6E: the characters p, e and n. That value is an accident of
the layout; another compiler or another flag could print something else, or crash.
Clang’s link-time optimised build (-flto) is just as silent. GCC’s is not: with -flto, GCC compares the types
and declarations that the translation units share, and its -Wodr warning, which is on by default, names the type,
the file and the first field that differs. That makes a GCC LTO build in continuous integration a cheap safety net.
The everyday defences are simpler: build every file of a program with the same flags, never change a type’s members
with a macro that one file defines and another does not, and give each definition one home in one header.
Checkpoint
Check: Breaks that build without an error
Every linker reports the same two errors in its own words
The errors on this page come from Apple’s linker, which both compilers use on macOS. Other linkers word them differently but mean the same thing:
| Linker | A used name is defined nowhere | A name is defined twice |
|---|---|---|
| Apple’s ld (macOS) | Undefined symbols for architecture arm64 |
duplicate symbol |
| GNU ld (the usual linker on Linux) | undefined reference to the name |
multiple definition of the name |
| Microsoft’s link.exe | LNK2019: unresolved external symbol |
LNK2005, “already defined in” the other object file |
Whatever the wording, ask the same two questions: which object file should define this name, and was that file
compiled and given to the linker? For an undefined name, the answer is often a .cpp file left off the link command,
or a template whose body no file could see; for a duplicate, a definition in a header. Microsoft’s documentation of
LNK2005 states the cause in the terms of this lesson: the one-definition rule was broken.
Checkpoint
Check: Reading link errors
Practise: split a single-file program
This program works, but everything is in one file:
// grades_single.cpp: everything in one file. Split it into grades.h, grades.cpp and main.cpp.
#include <iostream>
#include <string>
#include <vector>
struct Student {
std::string name;
int marks; // out of 100
};
constexpr int pass_mark = 40;
char grade(int marks) {
if (marks >= 90) return 'A';
if (marks >= 75) return 'B';
if (marks >= 60) return 'C';
if (marks >= pass_mark) return 'D';
return 'F';
}
double average(const std::vector<Student>& students) {
if (students.empty()) return 0.0;
int total = 0;
for (const Student& s : students) total += s.marks;
return static_cast<double>(total) / students.size();
}
int main() {
std::vector<Student> batch{{"Asha", 91}, {"Ravi", 58}, {"Meera", 76}, {"Kabir", 34}};
for (const Student& s : batch) {
std::cout << s.name << ": " << s.marks << " (" << grade(s.marks) << ")\n";
}
std::cout << "average " << average(batch) << ", pass mark " << pass_mark << '\n';
} Split it into three files:
grades.hwith an include guard (or#pragma once), theStudenttype, the pass mark and the declarations ofgrade()andaverage(). Decide how to keep the pass mark one constant for the whole program.grades.cpp, which includesgrades.hand holds the two function definitions.main.cpp, which includesgrades.hand holds onlymain().
Then compile each .cpp file with -c, link the two object files, and check three things: nm shows grade and
average with T in grades.o and U in main.o, the program prints exactly what the single file printed, and
the build still works if you include grades.h twice in main.cpp. Here is one possible split, built both ways:
C header · grades_split/grades.h
// grades.h: what other files need to know. Declarations, the type and the constant.
#ifndef GRADES_H
#define GRADES_H
#include <string>
#include <vector>
struct Student {
std::string name;
int marks; // out of 100
};
// constexpr at namespace scope implies const, so each file gets its own copy of
// this constant; inline makes it one object, which is the clearer choice for a shared value.
inline constexpr int pass_mark = 40;
char grade(int marks);
double average(const std::vector<Student>& students);
#endif // GRADES_H C++ (grades.cpp) · grades_split/grades.cpp
// grades.cpp: the one place where grade() and average() are defined.
#include "grades.h"
char grade(int marks) {
if (marks >= 90) return 'A';
if (marks >= 75) return 'B';
if (marks >= 60) return 'C';
if (marks >= pass_mark) return 'D';
return 'F';
}
double average(const std::vector<Student>& students) {
if (students.empty()) return 0.0;
int total = 0;
for (const Student& s : students) total += s.marks;
return static_cast<double>(total) / students.size();
} C++ (main.cpp) · grades_split/main.cpp
// main.cpp: uses the functions through the header only.
#include <iostream>
#include <vector>
#include "grades.h"
int main() {
std::vector<Student> batch{{"Asha", 91}, {"Ravi", 58}, {"Meera", 76}, {"Kabir", 34}};
for (const Student& s : batch) {
std::cout << s.name << ": " << s.marks << " (" << grade(s.marks) << ")\n";
}
std::cout << "average " << average(batch) << ", pass mark " << pass_mark << '\n';
} from session import run
# The single file first, then the split version: each .cpp file compiled on its own, then linked.
run("""
clang++ -std=c++23 -Wall -Wextra grades_single.cpp -o single && ./single
clang++ -std=c++23 -Wall -Wextra -c grades_split/grades.cpp -o grades.o
clang++ -std=c++23 -Wall -Wextra -c grades_split/main.cpp -o main.o
nm -C grades.o | grep -E ' T (grade|average)'
nm -C main.o | grep -E ' U (grade|average)'
clang++ grades.o main.o -o split && ./split
g++-16 -std=c++23 -Wall -Wextra -c grades_split/grades.cpp -o grades.o
g++-16 -std=c++23 -Wall -Wextra -c grades_split/main.cpp -o main.o
g++-16 grades.o main.o -o split && ./split
""") Output
$ clang++ -std=c++23 -Wall -Wextra grades_single.cpp -o single && ./single
Asha: 91 (A)
Ravi: 58 (D)
Meera: 76 (B)
Kabir: 34 (F)
average 64.75, pass mark 40
$ clang++ -std=c++23 -Wall -Wextra -c grades_split/grades.cpp -o grades.o
$ clang++ -std=c++23 -Wall -Wextra -c grades_split/main.cpp -o main.o
$ nm -C grades.o | grep -E ' T (grade|average)'
0000000000000000 T grade(int)
0000000000000090 T average(std::__1::vector<Student, std::__1::allocator<Student>> const&)
$ nm -C main.o | grep -E ' U (grade|average)'
U grade(int)
U average(std::__1::vector<Student, std::__1::allocator<Student>> const&)
$ clang++ grades.o main.o -o split && ./split
Asha: 91 (A)
Ravi: 58 (D)
Meera: 76 (B)
Kabir: 34 (F)
average 64.75, pass mark 40
$ g++-16 -std=c++23 -Wall -Wextra -c grades_split/grades.cpp -o grades.o
$ g++-16 -std=c++23 -Wall -Wextra -c grades_split/main.cpp -o main.o
$ g++-16 grades.o main.o -o split && ./split
Asha: 91 (A)
Ravi: 58 (D)
Meera: 76 (B)
Kabir: 34 (F)
average 64.75, pass mark 40
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grades_session.py
The split version prints the same five lines with both compilers, and its header keeps the pass mark one object with
inline constexpr. Now practise the seven ideas of this lesson: the session starts with the one you are least sure
of, and every answer, right or wrong, comes with its reason:
Practice that adapts to you
Practise this lesson
16 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between a declaration and a definition, and which of them goes in a header?
A declaration introduces a name and its type, so the compiler can check calls to it: char grade(int marks); or
extern int orders_seen;. A definition also creates the thing: the function’s body, or the variable’s storage. A
program may declare a function as often as it likes, but must define an ordinary function or variable exactly once,
which is the one-definition rule. So a header, which is pasted into many source files, holds declarations, plus the
definitions the rule allows in every file: types, inline and constexpr functions, inline variables and
templates. The bodies of ordinary functions and the definitions of ordinary variables go in exactly one .cpp file.
Interviewers often follow this question with the exceptions to the rule. Decide each one before you read the notes on every option:
Checkpoint
Interview round: two follow-up questions
Key takeaways
#includepastes a header’s text into the file; each.cppfile with its headers is a translation unit, compiled on its own, and the linker matches every used symbol to one definition.- Every header needs an include guard or
#pragma once; either protects one translation unit, not the program. - The program needs exactly one definition of each ordinary function and variable: none gives an undefined symbol, two give a duplicate symbol, whatever words the linker uses for them.
- Share a variable with
externin the header and one definition in a.cppfile, or with aninlinevariable; aconstorconstexprat namespace scope is one copy per file unless it is alsoinline. - Templates,
inlineandconstexprfunctions and member functions defined in their class go in headers, so every file that needs one can see its body. - Different definitions of an inline function or a type in two files build without an error and give wrong results;
GCC’s
-fltobuild reports differing types.
Terms in this lesson
- explicit instantiation
- A line such as template int largest(const std::vector<int>&); in the file that holds a template's body. That file then writes the code of the named instantiation, so other files can link to it without seeing the body.
- include guard
- The lines #ifndef NAME and #define NAME at the top of a header, with #endif at its end, so that a second #include of the header in the same translation unit is skipped. It protects one translation unit, not the whole program.
- inline variable
- A variable declared inline, as in inline int orders_seen = 0; (C++17). It may be defined in every translation unit that includes its header, and the linker keeps one object for the whole program.
References
- Working Draft, Programming Languages: C++ (N4950), 6.3 One-definition rule and 9.2.8 The inline specifier (ISO/IEC JTC1/SC22/WG21)
- The C Preprocessor: Once-Only Headers (GNU Project)
- The C Preprocessor: Alternatives to Wrapper #ifndef (GNU Project)
- The C Preprocessor: Search Path (GNU Project)
- Warning Options (-Wodr) (GNU Project)
- Clang CFE Internals Manual: the MultipleIncludeOpt class (LLVM Project)
- once pragma (Microsoft)
- Linker Tools Error LNK2005 (Microsoft)
- Linker Tools Error LNK2019 (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress