Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 20 minutes

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:

Where each kind of code belongs
Criterion In a headerIn 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 usetypes used by that file only
Inline and constexpr functions, templates the whole definition, body includedonly those that no other file uses
When to choose Anything a second .cpp file must seeAnything 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:

grades.h is pasted into grades.cpp and main.cpp; grades.o defines the functions, main.o only uses them, and the linker joins them.grades.hdeclares grade(), average();defines Student, pass_markUnit 2grades.cpp+ text of grades.hUnit 1main.cpp+ text of grades.hgrades.oT grade(int)T average(...)main.oU grade(int)U average(...)Linkermatches every Uto exactly one Tsplitexecutable#include#includecompilecompile

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.

  1. The header grades.h declares the functions grade() and average() and defines the type Student and the constant pass_mark.
  2. 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.
  3. 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.
  4. 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

  1. Question 1 of 2 grades.cpp and main.cpp both include grades.h. Which of these belongs in grades.h?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The declaration char grade(int marks);

    A header is pasted into every translation unit that includes it, so it holds what each of them must see and may appear many times: declarations, types and the few definitions the rule allows in every file. The bodies of ordinary functions and the definitions of ordinary variables live in exactly one .cpp file.

  2. Question 2 of 2 In the split grades build, nm prints U grade(int) for main.o. What does that line mean?

    Choose one answer.

    A hint

    Compare it with what nm printed for grades.o.

    Show the answer: Question 2 of 2

    Answer: main.o uses grade but does not define it, so the linker needs exactly one object file that does, here grades.o with T grade(int)

    nm marks a symbol an object file defines in its code with T and one it only uses with U. The linker's job is to find exactly one T for every U: here main.o uses grade(int) and average(), and grades.o defines them.

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:

Two paths to 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() {}
Compiling report.cpp with both compilers Python · diamond_session.py
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 with an include guard C header · diamond_fixed/money.h
// 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
The same build with the guard Python · diamond_fixed_session.py
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:

Interactive · step by step report.cpp through the preprocessor, without and with a guard Step through the two #include lines of report.cpp and count how many times the text of money.h reaches the translation unit.
report.cpp through the preprocessor, without and with a guard, step 7 of 7. Showing report.cpp, Translation unit (Money defined once), invoice.h, refund.h, money.h, first time (defines MONEY_H and Money), money.h, second time (MONEY_H defined: skipped). In focus: Translation unit.line 2line 3#include#includereport.cpptwo #include linesTranslation unitMoney defined onceinvoice.hincludes money.hrefund.hincludes money.hmoney.h, first timedefines MONEY_H and Moneymoney.h, second timeMONEY_H defined: skipped

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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; }.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 report.cpp includes invoice.h and refund.h, and both of them include money.h, which now has an include guard. How many definitions of Money reach the translation unit of report.cpp?

    Type a number.

    A hint

    Follow MONEY_H through the two inclusions.

    Show the answer: Question 1 of 2

    Answer: 1 definition

    The first inclusion, through invoice.h, finds MONEY_H undefined, defines it and pastes Money. The second, through refund.h, finds MONEY_H defined and skips everything up to #endif. So the unit holds one definition, and both compilers print compiled.

  2. Question 2 of 2 Which guard macro is a safe choice for the header shop/money.h?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: SHOP_MONEY_H

    A guard macro must be unique, so it is built from the file name, such as MONEY_H or SHOP_MONEY_H. Names with an underscore and a capital at the start, or with two underscores in a row, are reserved for the implementation.

A program needs exactly one definition of each function and variable

The standard’s rule, in plain words, has three parts:

  1. Inside one translation unit, nothing may be defined twice. The diamond above broke this part, and the compiler reported it.
  2. 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.
  3. Types, inline functions, inline variables 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:

A counter defined in a header

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';
}
Each file compiles, the link fails Python · orders_session.py
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:

Two ways to share one variable

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
Both fixes link and count two orders Python · orders_fixed_session.py
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:

Interactive · step by step Two definitions of orders_seen, then the two fixes Step through the build of take.cpp and main.cpp. Watch what each object file holds for orders_seen, and what the linker does with it.
Two definitions of orders_seen, then the two fixes, step 6 of 6. Showing take.cpp (pastes inline int …), main.cpp (pastes inline int …), take.o (inline orders_seen), main.o (inline orders_seen), Linker (keeps one copy), orders_demo (orders seen: 2). In focus: take.o, main.o, Linker.compilecompilelinktake.cpppastes inline int …main.cpppastes inline int …take.oinline orders_seenmain.oinline orders_seenLinkerkeeps one copyorders_demoorders seen: 2

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 A link fails with duplicate symbol '_total'. What does that tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Two of the object files given to the linker each define total

    The linker reports a duplicate symbol when two object files define the same ordinary function or variable, usually because a header that several files include holds a definition. The fix is one definition in one .cpp file, or an inline variable.

  2. Question 2 of 2 orders.h defines int orders_seen = 0; and both take.cpp and main.cpp include it. Which changes make the program link and print orders seen: 2?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • extern int orders_seen; in orders.h, and int orders_seen = 0; in take.cpp only
    • inline int orders_seen = 0; in orders.h

    The two clean fixes are a declaration in the header with one definition in a .cpp file, and an inline variable. Both give one object, which both calls of take_order() increment. A static variable links too, but as a copy per file, so the count goes missing.

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 C header · rates/settings.h
// 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
Comparing addresses from two files

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';
}
One object against one copy per file Python · rates_session.py
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:

Interactive · answer and compare How should a header share this value? Answer three questions about the value your header shares. The table marks the declarations that fit and says why the others do not.
Must the value change while the program runs, as a counter does?
Must every file see the same object, so that a change or an address in one file holds in all?
Should the header hold the whole definition, with no .cpp file to own it?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Six ways to put a shared variable or constant in a header, against the four things that decide between them
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

  1. Question 1 of 2 settings.h defines inline int log_level = 1; and is included by network.cpp and main.cpp. How many objects named log_level does the program hold?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 1 object

    An inline variable is one object for the whole program: the linker keeps one copy of the weak external symbol and drops the rest. That is why the session printed log_level is one object: true, and why network.cpp saw the 3 that main.cpp stored.

  2. Question 2 of 2 grades.h has inline constexpr int pass_mark = 40;. What changes if it says constexpr int pass_mark = 40; instead?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Each file that includes grades.h gets its own copy, and the program still works because every copy is 40

    Without inline, a namespace-scope constexpr variable is a private copy in each translation unit. That is harmless for one int, but it is two objects at two addresses; inline constexpr makes it one object, which is why grades.h uses it.

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:

A template defined in a source 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';
}
Undefined symbols for both instantiations Python · largest_session.py
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 with explicit instantiations C++ · largest_explicit/largest.cpp
// 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>&);
Both fixes Python · largest_fixed_session.py
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:

Interactive · step by step Where largest<int> is made, and where it is not Step through the build of largest.cpp and main.cpp, then the two fixes. Watch which file can see the body of the template when it is needed.
Where largest<int> is made, and where it is not, step 6 of 6. Showing largest.cpp (two explicit instantiations), main.cpp (calls largest<int>, <double>), largest.o (T largest<int>, <double>), main.o (U largest<int>, U <double>), Linker (matches each U to a T), largest_demo (prints 9 and 7.25). In focus: Linker.compilecompilelinklargest.cpptwo explicit instantiationsmain.cppcalls largest<int>, <double>largest.oT largest<int>, <double>main.oU largest<int>, U <double>Linkermatches each U to a Tlargest_demoprints 9 and 7.25

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 largest.h declares the template largest, largest.cpp defines it, and main.cpp calls it on a vector of int. Which tool reports the problem?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The linker: Undefined symbols for largest<int> and largest<double>

    The template's code is written only where a translation unit uses it while it can see the body. main.cpp uses it without the body, largest.cpp has the body without a use, so no file makes largest<int> and the linker reports it undefined.

  2. Question 2 of 2 largest.cpp keeps the body and explicitly instantiates largest<int> and largest<double>. A new file calls largest on a std::vector<long>. What happens?

    Choose one answer.

    A hint

    Which file could make largest<long>?

    Show the answer: Question 2 of 2

    Answer: The link fails with an undefined symbol for largest<long>, because no file can make it

    Explicit instantiation suits a library with a fixed list of types. Any other type needs its own line in largest.cpp, or the body in the header, so that the using file can instantiate it.

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:

Two teams, one name

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';
}
Four builds, three different answers Python · units_session.py
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:

A struct whose layout depends on a macro

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';
}
A wrong value, then GCC's link-time check Python · item_session.py
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:

Interactive · step by step One Item, two layouts item.cpp is built with -DWITH_LABEL and main.cpp without it. Step through what each file thinks an Item holds, byte by byte.
One Item, two layouts, step 5 of 5. Showing id = 7, label = "pen", qty = 25, id (id = 7), qty (qty = 7234928). In focus: qty = 25.same bytesread as qtyid = 7bytes 0 to 3label = "pen"bytes 4 to 11qty = 25bytes 12 to 15idid = 7qtyqty = 7234928GCC -flto: -Wodr warning

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. id is right. it.id reads bytes 0 to 3 in both views, so main.cpp prints id 7, as it should.
  4. 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.
  5. 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.

Programmer Calculator (Bitwise) Enter 7234928 to see it as 0x6E6570, then read the three bytes as character codes.

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.

Text Diff Save the output of g++-16 -E for item.cpp with and without -DWITH_LABEL and compare the two struct definitions line by line.

Checkpoint

Check: Breaks that build without an error

  1. Question 1 of 2 In the units session, which build printed scale.cpp says kg, main.cpp says lb?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Clang at -O2, because each file inlined its own unit()

    Without optimisation, both files called unit(), and the linker kept the definition from whichever object file came first. With -O2, each file inlined its own body. Three answers from four builds, and no warning: the program breaks the rule in every one of them.

  2. Question 2 of 2 Which practices stop the Item layout bug from reaching production?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • A GCC -flto build in continuous integration
    • Build every file of a program with the same flags
    • Never let a macro that only some files define change a type's members

    A compiler sees one translation unit at a time, so only a check across units can catch differing definitions. Same flags and no layout macros prevent the bug; a GCC LTO build detects it if it happens anyway.

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.

Hex Viewer & File Inspector Open a .o file from these builds to see the object-file format and the symbol names stored inside it.

Checkpoint

Check: Reading link errors

  1. Question 1 of 2 On Linux, GNU ld prints multiple definition of `orders_seen'. Which message of Apple's linker reports the same problem?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: duplicate symbol '_orders_seen'

    Apple's ld says duplicate symbol, GNU ld multiple definition of and link.exe LNK2005: three wordings of one problem, two object files that define the same name.

  2. Question 2 of 2 Microsoft's linker reports LNK2019: unresolved external symbol for a function your program calls. What do you check first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Which .cpp file should define the function, and whether that file was compiled and given to the linker

    Whatever the linker, ask which object file should define the name and whether it was built and linked. For a template, also ask whether any file could see the body when it needed the instantiation.

Practise: split a single-file program

This program works, but everything is in one file:

grades_single.cpp C++ · grades_single.cpp
// 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:

  1. grades.h with an include guard (or #pragma once), the Student type, the pass mark and the declarations of grade() and average(). Decide how to keep the pass mark one constant for the whole program.
  2. grades.cpp, which includes grades.h and holds the two function definitions.
  3. main.cpp, which includes grades.h and holds only main().

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:

One possible split

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';
}
The single file and the split version Python · grades_session.py
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.

  1. Question 1 of 16 Which of these is a declaration but not a definition?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: char grade(int marks);

    A declaration introduces a name and its type; a definition also creates the thing: the body of a function, the storage of a variable, the members of a type. Headers hold declarations plus the definitions the rule allows everywhere.

  2. Question 2 of 16 src/report.cpp writes #include <money.h>, money.h sits next to it in src/, and the build has no -I option. What happens with GCC or Clang?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: The compiler does not find money.h, because angle brackets skip the folder of the including file

    Use quotes for your own headers and angle brackets for the standard library and installed libraries. With quotes, GCC and Clang look first in the folder of the file that contains the #include, then in the -I folders, then in the system folders.

  3. Question 3 of 16 A header has an include guard and defines int total = 0;. Two .cpp files include it. What happens when you build the program?

    Choose one answer.

    Show the answer: Question 3 of 16

    Answer: Both files compile, and the link fails with a duplicate symbol for total.

    An include guard works inside one translation unit only. Each .cpp file is preprocessed on its own, starts with no macros defined and gets its own copy of the header, so both object files define total and the linker refuses to choose. Declare it with extern in the header and define it in one .cpp file, or make it an inline variable.

  4. Question 4 of 16 Which statement about #pragma once is true?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: It is not part of the C++ standard, but GCC, Clang and Microsoft's compiler support it

    #pragma once and an include guard do the same job, one paste per translation unit. What matters is that every header has one of them.

  5. Question 5 of 16 Which of these may be defined in a header that ten .cpp files include, without breaking the one-definition rule?

    Choose every answer that is right.

    Show the answer: Question 5 of 16

    Answer:

    • inline int twice(int n) { return 2 * n; }
    • struct Point { int x; int y; };
    • template <typename T> T twice(T n) { return n + n; }

    Class types, inline functions and templates may be defined in every translation unit, as long as every copy is identical, which a shared header guarantees. An ordinary function and an ordinary variable need exactly one definition in the whole program, so defined in a header they become ten definitions and a duplicate symbol.

  6. Question 6 of 16 For which break of the one-definition rule does the standard require a diagnostic?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: Two definitions of one type inside a single translation unit

    Outside C++20 modules, the standard requires no diagnostic for parts 2 and 3 of the rule, the parts that span translation units. A compiler sees one unit at a time, so what you get depends on what the linker checks.

  7. Question 7 of 16 const int max_retries = 3; sits in a header that 12 .cpp files of one program include. How many objects named max_retries does the program contain?

    Type a number.

    Show the answer: Question 7 of 16

    Answer: 12 objects

    A const variable at namespace scope has internal linkage, so each translation unit gets its own private copy: 12 files, 12 objects, at 12 addresses, as the settings session showed for two files. Write inline const int (or inline constexpr int) to make it one object for the whole program.

  8. Question 8 of 16 nm -m shows log_level as a weak external symbol. What does weak tell the linker?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: It may meet the symbol in several object files, and should keep one copy

    Inline functions and inline variables are stored as weak definitions, so the linker expects copies and keeps one. A const variable at namespace scope is non-external instead: each file's private copy.

  9. Question 9 of 16 A header must share a page-view counter that every .cpp file increments, and the team wants no .cpp file whose only job is to define it. Which line goes in the header?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: inline int views = 0;

    A value that changes and must be one object is an inline variable, or an extern declaration with one definition in a .cpp file. Without a .cpp file to own it, the inline variable is the one that fits.

  10. Question 10 of 16 largest.h declares template <typename T> T largest(const std::vector<T>&); and largest.cpp defines it. main.cpp calls largest on a vector of int. Which error do you get, and from which tool?

    Choose one answer.

    Show the answer: Question 10 of 16

    Answer: An undefined symbol for largest<int>, from the linker.

    main.cpp sees only the declaration, so it compiles a call and leaves an undefined symbol. largest.cpp has the body, but nothing in that file asks for largest<int>, so it emits no code at all. The linker finds no definition. Put the template in the header, or add an explicit instantiation for int in largest.cpp.

  11. Question 11 of 16 What does the line template double largest(const std::vector<double>&); at the end of largest.cpp do?

    Choose one answer.

    Show the answer: Question 11 of 16

    Answer: It makes largest.cpp write the code of largest<double>, so other files can link to it

    Explicit instantiation keeps the body in one .cpp file and compiles it once for each listed type, which suits a library that supports a fixed list of types.

  12. Question 12 of 16 In the "Four builds, three different answers" session, why did swapping the order of the two files change what the -O0 program printed?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: The linker kept only one of the two inline definitions of unit(), the one from the first object file.

    Two inline definitions of one function are allowed only when they are identical, so the linker expects copies and keeps one. In these runs it kept the copy from the first object file, and both files called that one. With -O2 each file inlined its own body instead. The program breaks the one-definition rule, and no tool was required to report it.

  13. Question 13 of 16 Why does the linker accept two different definitions of the inline function unit() without a word?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: Inline functions may be defined in many object files, so it expects copies and keeps one without comparing them

    For names the rule allows in many translation units, the linker keeps one definition and drops the others. If they differ, the program is wrong, and the standard requires no diagnostic.

  14. Question 14 of 16 Suppose item.cpp had used the label "ab" instead of "pen", so bytes 4 to 7 hold 0x61 0x62 0x00 0x00. What would main.cpp print for qty?

    Type a number.

    A hint

    Read the four bytes as one int, lowest byte first, as the lesson read 0x70 0x65 0x6E 0x00.

    Show the answer: Question 14 of 16

    Answer: 25185

    main.cpp reads qty from bytes 4 to 7, where the label starts. Lowest byte first, 0x61 0x62 0x00 0x00 is the int 0x6261, which is 0x61 + 0x62 × 256 = 97 + 25,088 = 25,185.

  15. Question 15 of 16 GNU ld prints undefined reference to `average(...)'. What does it mean?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: An object file uses average, and no object file given to the linker defines it

    An undefined reference means a use without a definition: the defining .cpp file was not compiled or not linked, or, for a template, no file could see the body when it needed the instantiation.

  16. Question 16 of 16 Linking main.o on its own fails with an undefined symbol for average(), which grades.cpp defines. What is the right fix?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: Compile grades.cpp too, and give grades.o to the linker with main.o

    Each .cpp file is compiled on its own and the linker needs every object file that defines something the program uses: clang++ grades.o main.o -o split, as the practice session does.

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

  1. Question 1 of 2 The interviewer asks: why may a header that ten .cpp files include define inline int twice(int n) { return 2 * n; }, but not int twice(int n) { return 2 * n; }?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An inline function may be defined in every translation unit if every definition is identical, and the linker keeps one copy; the ordinary one becomes ten definitions and a duplicate symbol

    The one-definition rule allows inline functions, inline variables, types and templates to be defined in many translation units, provided every definition is identical. For an ordinary function, ten includes mean ten definitions, which the linker reports.

  2. Question 2 of 2 Then: a struct defined in that header is also defined once in each of the ten files. Why is that allowed, and what must stay true?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A type may be defined in every translation unit, but every definition must be identical, or the program is wrong without any diagnostic, as the Item layout showed

    Types, like inline functions and templates, may be defined in many translation units, which is why classes live in headers. The price is that every definition must be the same token for token, with every name meaning the same thing, and no tool is required to check it.

Key takeaways

  • #include pastes a header’s text into the file; each .cpp file 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 extern in the header and one definition in a .cpp file, or with an inline variable; a const or constexpr at namespace scope is one copy per file unless it is also inline.
  • Templates, inline and constexpr functions 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 -flto build 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.