C++ Module 4 – Functions, files and scope
Scope, storage duration, linkage and namespaces
When C++ variables are created and destroyed, which names other files can see, how namespaces and lookup work, and how to avoid start-up order bugs.
What you will learn
- Predict when local, static, thread-local and global variables are created and destroyed
- Use an unnamed namespace or static to keep a name private to its file
- Fix a start-up order bug with a function-local static or constinit
- Predict which function an unqualified call finds through argument-dependent lookup
- Explain why a using-directive never belongs in a header
Before you start
What you’ll learn · Before you start
What you will learn
- Predict when local, static, thread-local and global variables are created and destroyed
- Use an unnamed namespace or static to keep a name private to its file
- Fix a start-up order bug with a function-local static or constinit
- Predict which function an unqualified call finds through argument-dependent lookup
- Explain why a using-directive never belongs in a header
Before you start
On this page
Link two files that each define their own attempts counter and valid_amount() helper, and the linker stops with 2
duplicate symbols, although neither file ever mentions the other. Initialise a global from a global of another file,
and the same program prints doubled_rate 36 or doubled_rate 0 depending only on the order of the files on the link
command. Both bugs come from three properties of every name that this lesson keeps apart.
You will predict when each kind of variable is created and destroyed, make a name private to its file, fix a start-up order bug two ways, predict which function an unqualified call finds, and see why one line of a header can break code that never wrote it. Apple clang 21 built every program, and GCC 16 more than half of them.
Scope, storage duration and linkage answer three different questions
Every variable in C++ has three separate properties, and most confusion comes from mixing them up. Its
scope: The part of a program in which a name can be used. A name declared inside a block, the code between a pair of braces, can be used from its declaration up to the block's closing brace. More in “C++ program structure, naming and formatting” is the part of the source code where its name can be used: a block, a function, a class,
a namespace. Its storage duration: How long an object exists while a program runs: automatic (until the end of its block), static (the whole run), thread (the life of one thread) or dynamic (from new until delete). It belongs to the object, while scope belongs to its name. is how long the object itself exists while the
program runs: until the end of the block (automatic), for the whole run (static), for the life of one thread (thread),
or until you delete it (dynamic). Its linkage: Whether other translation units can refer to a name: external linkage means every file can, internal linkage means the name is private to its file, and no linkage means it is a local name. decides whether other translation units, other
.cpp files, can refer to the same name: external linkage means yes, 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. means the name is private to
its file, and no linkage means it is local. namespace: A named scope that groups names so that they cannot collide: the standard library's names are in namespace std, so std::count and a program's own count are two different names. More in “C++ program structure, naming and formatting” group names under a prefix such as
std:: so that libraries do not collide.
The table answers the three questions for the declarations of this lesson:
| Declaration | Scope | Storage | Linkage |
|---|---|---|---|
int n = 0; in a function |
block | automatic | none |
static int issued = 0; in a function |
block | static | none |
int attempts = 0; at namespace scope |
namespace | static | external |
static int attempts = 0; there, or the same in an unnamed namespace |
namespace | static | internal |
const int max_retries = 3; at namespace scope |
namespace | static | internal, unless inline or extern |
thread_local int calls = 0; at namespace scope |
namespace | thread | external |
In the first two rows the name ends at the function’s closing brace; in the others it is usable anywhere in the file
after its declaration. The keyword static means two different things here. Inside a function it changes the
storage duration and leaves the scope alone. At namespace scope, where storage is static anyway, it changes the
linkage to internal.
Checkpoint
Check: Scope, storage and linkage
Storage duration decides when an object is created and destroyed
Tracer prints a line when it is created and when it is destroyed, which makes lifetimes visible:
// lifetimes.cpp: when objects of each storage duration are created and destroyed.
#include <iostream>
#include <string>
struct Tracer {
std::string name;
explicit Tracer(std::string n) : name(std::move(n)) { std::cout << " create " << name << '\n'; }
~Tracer() { std::cout << " destroy " << name << '\n'; }
};
Tracer global{"global (static storage)"}; // created before main() starts
void visit() {
static Tracer once{"static local in visit()"}; // created the first time control gets here
Tracer each{"local in visit()"}; // created on every call, destroyed at the closing brace
}
int main() {
std::cout << "main starts\n";
{
Tracer block{"local in a block"};
std::cout << " ...inside the block\n";
} // block is destroyed here, at the end of its scope
std::cout << "first call\n";
visit();
std::cout << "second call\n";
visit();
std::cout << "main returns\n";
} // then static objects are destroyed, in the reverse order of their creation from session import run
run("""
clang++ -std=c++23 -Wall -Wextra lifetimes.cpp -o lifetimes && ./lifetimes
""") Output
$ clang++ -std=c++23 -Wall -Wextra lifetimes.cpp -o lifetimes && ./lifetimes create global (static storage) main starts create local in a block ...inside the block destroy local in a block first call create static local in visit() create local in visit() destroy local in visit() second call create local in visit() destroy local in visit() main returns destroy static local in visit() destroy global (static storage)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifetimes_session.py
Lifetimes of global, local and static local objects in one run
Text description of the diagram
The diagram follows one run of lifetimes.cpp from top to bottom.
- The program starts. All static storage is first filled with zeros, then the global object is constructed, before main() begins.
- main() begins.
- A block runs. Its local object is created when its declaration is reached and destroyed at the closing brace of the block.
- The first call of visit() creates the function's static local object, once, and also creates and destroys the function's ordinary local object.
- The second call of visit() creates and destroys only the ordinary local object; the static local object already exists.
- main() returns.
- The program ends. Objects with static storage duration are destroyed in the reverse order of their creation: the static local object first, then the global.
The diagram shows the phases of the run. Step through the printed lines and watch which of the four objects exist at each one:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Before main. The first line, create global (static storage), is printed before main starts: an object with static storage at namespace scope is built before main runs.
- A block opens. main prints main starts, then enters its block: block is created at its declaration, and ...inside the block is printed while it exists.
- The block closes. At the closing brace of the block, block is destroyed: destroy local in a block. Its name and its object end at the same brace.
- First call of visit(). Control reaches static Tracer once for the first time, so once is created now. Then each is created, as on every call.
- End of the first call. At the end of visit(), each is destroyed. once is not: a static local lives until the program ends.
- Second call of visit(). The second call creates and destroys a new each. There is no second create line for once: its initialisation ran once, on the first call.
- After main returns. Statics are destroyed in the reverse order of their creation: once, created last, goes first, then global. Those are the last two lines of the output.
- The global was created before
mainprinted anything, and destroyed aftermainreturned. - The block’s local died at its closing brace, and
visit()’s ordinary local was created and destroyed on each call. visit()’s static local was created on the first call only, and survived until the end of the program.- At exit, static objects were destroyed in the reverse order of their creation: the static local, created last, went first.
The fourth kind, dynamic storage, is for objects you create with new and destroy with delete, or better with
smart pointers; later lessons in this track cover it.
Checkpoint
Check: When objects live and die
A static local is created once, even when eight threads race to it
A static local is initialised once and keeps its value, which makes it the simplest counter or cache that belongs to one function:
// counter.cpp: a static local keeps its value between calls; an ordinary local does not.
#include <iostream>
int next_ticket() {
static int issued = 0; // initialised once, the first time the function runs
return ++issued;
}
int not_a_counter() {
int issued = 0; // a new variable, set to 0, on every call
return ++issued;
}
int main() {
for (int i = 0; i < 3; ++i) {
std::cout << "ticket " << next_ticket() << ", not a counter " << not_a_counter() << '\n';
}
} Since C++11, its initialisation is also thread-safe. The standard says that the first thread to reach the
declaration runs the initialiser and every other thread that arrives meanwhile waits for it to finish. Eight threads
call shared_table() at once here, and the constructor sleeps for 50 ms to give them every chance to collide:
// once_threads.cpp: eight threads race to the first call of shared_table().
// Since C++11 the static local is initialised exactly once; the others wait for it.
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<int> constructions{0};
struct Table {
Table() {
++constructions;
std::this_thread::sleep_for(std::chrono::milliseconds(50)); // a slow start makes the race likely
}
int rows = 100;
};
Table& shared_table() {
static Table table; // thread-safe initialisation
return table;
}
std::atomic<int> rows_seen{0};
void reader() {
rows_seen += shared_table().rows;
}
int main() {
std::vector<std::jthread> threads;
for (int i = 0; i < 8; ++i) {
threads.emplace_back(reader);
}
threads.clear(); // joins every thread
std::cout << "constructions: " << constructions << ", rows seen: " << rows_seen << '\n';
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra counter.cpp -o counter && ./counter
clang++ -std=c++23 -Wall -Wextra -fsanitize=thread once_threads.cpp -o once && ./once
g++-16 -std=c++23 -Wall -Wextra once_threads.cpp -o once && ./once
""") Output
$ clang++ -std=c++23 -Wall -Wextra counter.cpp -o counter && ./counter ticket 1, not a counter 1 ticket 2, not a counter 1 ticket 3, not a counter 1 $ clang++ -std=c++23 -Wall -Wextra -fsanitize=thread once_threads.cpp -o once && ./once constructions: 1, rows seen: 800 $ g++-16 -std=c++23 -Wall -Wextra once_threads.cpp -o once && ./once constructions: 1, rows seen: 800
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 counter_session.py
Step through the race between the first thread and the other seven:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The first arrival. One of the eight threads reaches static Table table; first. It starts the initialisation: the constructor counts constructions up to 1 and sleeps for 50 ms.
- The others wait. The other seven threads call shared_table() while the constructor is still sleeping. They reach the same declaration, see that its initialisation is running, and wait for it.
- Built once. The constructor returns. The table now exists with its 100 rows, and the first thread gets a reference to it.
- Everyone shares it. The waiting threads go on with the same object. Nobody runs the constructor a second time.
- The result. The program printed constructions: 1, rows seen: 800, eight readers of 100 rows each, and ThreadSanitizer reported no data race. The guarantee covers the initialisation only.
The table was built once, every thread saw its 100 rows, and Clang’s ThreadSanitizer (-fsanitize=thread), which
reports data races, found none. Three details of the rule are worth knowing. If the initialiser throws an exception,
the variable is not initialised, and the next call tries again. If the initialiser calls the same function
recursively, so that control reaches the declaration again before the first initialisation has finished, the
behaviour is undefined. And the guarantee covers the initialisation only: once threads share the object, changing it
still needs a mutex or an atomic.
One object per thread: thread_local
thread_local gives each thread its own copy of a variable, which lives as long as its thread:
// per_thread.cpp: thread_local gives each thread its own copy of a variable.
#include <atomic>
#include <functional>
#include <iostream>
#include <thread>
thread_local int calls_in_this_thread = 0; // one per thread, created when the thread starts using it
std::atomic<int> calls_in_all_threads{0}; // one for the whole program
void work(int times, int& report) {
for (int i = 0; i < times; ++i) {
++calls_in_this_thread;
++calls_in_all_threads;
}
report = calls_in_this_thread;
}
int main() {
int a = 0, b = 0;
{
std::jthread t1(work, 3, std::ref(a));
std::jthread t2(work, 5, std::ref(b));
} // both threads are joined here
std::cout << "thread 1 counted " << a << ", thread 2 counted " << b
<< ", main counted " << calls_in_this_thread << ", all threads " << calls_in_all_threads << '\n';
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=thread per_thread.cpp -o per_thread && ./per_thread
g++-16 -std=c++23 -Wall -Wextra per_thread.cpp -o per_thread && ./per_thread
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=thread per_thread.cpp -o per_thread && ./per_thread thread 1 counted 3, thread 2 counted 5, main counted 0, all threads 8 $ g++-16 -std=c++23 -Wall -Wextra per_thread.cpp -o per_thread && ./per_thread thread 1 counted 3, thread 2 counted 5, main counted 0, all threads 8
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 per_thread_session.py
Each thread counted only its own calls, and main’s copy stayed at 0 because main never called work. The
std::atomic total, shared by all threads, saw all 8.
Treating a thread-safe static as a thread-safe object
Only the first initialisation of static Table table; is protected. Two threads that then change table at the
same time still race; guard the changes with a mutex, or make the shared value an atomic.
Checkpoint
Check: Static locals and thread_local
Linkage decides which names another file can reach
A function or a non-const variable at namespace scope has external linkage by default: the linker treats every
use of that name, in every file, as the same thing. Two files that each pick the same name for their own private
helper therefore collide:
C++ (payments.cpp) · collide/payments.cpp
// payments.cpp: a file-wide counter and a helper, meant for this file only. Amounts are in paise.
int attempts = 0;
bool valid_amount(long paise) { return paise > 0; }
int record_payment(long paise) {
++attempts;
return valid_amount(paise) ? attempts : -1;
} C++ (refunds.cpp) · collide/refunds.cpp
// refunds.cpp: another file picked the same names for its own private use.
#include <iostream>
int attempts = 0;
bool valid_amount(long paise) { return paise > 0 && paise <= 500'000; }
int record_payment(long paise);
int main() {
++attempts;
std::cout << record_payment(25'000) << ' ' << attempts << ' ' << valid_amount(1'000'000) << '\n';
} payments.cpp (unnamed namespace) · collide_fixed/payments.cpp
// payments.cpp, fixed: the private names sit in an unnamed namespace, so they have internal linkage.
namespace {
int attempts = 0;
bool valid_amount(long paise) { return paise > 0; }
} // namespace
int record_payment(long paise) { // this one is meant for other files, so it stays outside
++attempts;
return valid_amount(paise) ? attempts : -1;
} refunds.cpp (static) · collide_fixed/refunds.cpp
// refunds.cpp, fixed: static at namespace scope gives internal linkage too.
#include <iostream>
static int attempts = 0;
static bool valid_amount(long paise) { return paise > 0 && paise <= 500'000; }
int record_payment(long paise);
int main() {
++attempts;
std::cout << record_payment(25'000) << ' ' << attempts << ' ' << valid_amount(1'000'000) << '\n';
} from session import run
# Two files define the same global names. Then the fixed version, and what nm shows:
# upper-case letters are external symbols, lower-case ones are local to their object file.
run("""
clang++ -std=c++23 -c collide/payments.cpp -o payments.o
clang++ -std=c++23 -c collide/refunds.cpp -o refunds.o
clang++ payments.o refunds.o -o collide_demo
clang++ -std=c++23 -c collide_fixed/payments.cpp -o payments.o
clang++ -std=c++23 -c collide_fixed/refunds.cpp -o refunds.o
clang++ payments.o refunds.o -o collide_demo && ./collide_demo
nm -C payments.o | grep -E 'attempts|valid_amount|record_payment'
""") Output
$ clang++ -std=c++23 -c collide/payments.cpp -o payments.o
$ clang++ -std=c++23 -c collide/refunds.cpp -o refunds.o
$ clang++ payments.o refunds.o -o collide_demo
duplicate symbol '_attempts' in:
refunds.o
payments.o
duplicate symbol 'valid_amount(long)' in:
refunds.o
payments.o
ld: 2 duplicate symbols
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
[exit status 1]
$ clang++ -std=c++23 -c collide_fixed/payments.cpp -o payments.o
$ clang++ -std=c++23 -c collide_fixed/refunds.cpp -o refunds.o
$ clang++ payments.o refunds.o -o collide_demo && ./collide_demo
1 1 0
$ nm -C payments.o | grep -E 'attempts|valid_amount|record_payment'
0000000000000000 T record_payment(long)
000000000000005c t (anonymous namespace)::valid_amount(long)
00000000000000b8 b (anonymous namespace)::attempts
This output changes from run to run: the linker may list the object files, and the duplicate names, in either order
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 collide_session.py
Step through the two links and watch which names the linker can see:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- External by default. Both files define attempts and valid_amount() at namespace scope. A function or a non-const variable there has external linkage, so the linker treats every use of the name, in every file, as the same thing.
- Two of each. The linker finds two definitions of each name: duplicate symbol _attempts and duplicate symbol valid_amount(long), 2 duplicate symbols.
- Made private. payments.cpp now puts the two names in an unnamed namespace, and refunds.cpp marks them static. Both give internal linkage: each file has its own attempts and valid_amount().
- Lower-case letters. nm lists payments.o's private names with lower-case letters, t for code and b for zero-filled data. The linker never matches them across files, so nothing clashes.
- The public function. record_payment() stays external, T in nm, because refunds.cpp calls it. The program prints 1 1 0: one attempt in each file, and refunds' stricter check rejected 1,000,000 paise.
With internal linkage each file has its own attempts and valid_amount(). The
output shows it: payments counted 1 attempt, refunds counted 1 of its own, and refunds’ stricter valid_amount()
rejected an amount above its limit (0 is how std::cout prints false). nm marks the private names with lower-case
letters, t for code and b for zero-initialised data, which the linker never matches across files.
record_payment stays external, T, because refunds.cpp calls it.
An unnamed namespace: A namespace written without a name, namespace { … }. Everything declared in it has internal linkage, so it is private to its file; unlike static, it works for types too. does the same as static and also works for types, which
static cannot make private. The C++ Core Guidelines recommend it for every entity that is internal to a file (rule
SF.22), and recommend against putting one in a header (SF.21), where it would give every including file a separate
copy.
Key idea
Every name at namespace scope is shared with the whole program unless you say otherwise: put a file’s private helpers and variables in an unnamed namespace.
Checkpoint
Check: Internal and external linkage
Globals in different files start up in no fixed order
At start-up, which on the usual compilers means before main begins, every variable with
static storage duration: The lifetime of a global, a variable at namespace scope or a static variable: it exists for the whole run of the program. Variables with thread storage duration, declared thread_local, exist once per thread instead. More in “C++ const, constexpr, consteval and constinit” is initialised in two steps. First all of them are
filled with zeros, or with their value if it is a constant the compiler can compute. Then the dynamic
initialisers run: those that need code at run time, such as a function call. Within one file they run in the order of
the definitions. Between files, the standard does not specify any order:
C++ (rate.cpp) · order/rate.cpp
// rate.cpp: a global whose value is computed when the program starts (dynamic initialisation).
int read_rate() {
return 18; // imagine this reads a configuration file
}
int base_rate = read_rate(); C++ (report.cpp) · order/report.cpp
// report.cpp: another global, initialised from the first one.
#include <iostream>
extern int base_rate; // defined in rate.cpp
int doubled_rate = base_rate * 2; // also dynamic: runs at start-up, in an unspecified order relative to rate.cpp
int main() {
std::cout << "base_rate " << base_rate << ", doubled_rate " << doubled_rate << '\n';
} from session import run
# The same two files, linked in both orders. Before main() runs, static storage is zero-filled,
# then each file's dynamic initialisers run, in an order the standard does not specify.
run("""
clang++ -std=c++23 -c order/rate.cpp -o rate.o
clang++ -std=c++23 -c order/report.cpp -o report.o
clang++ rate.o report.o -o order_demo && ./order_demo
clang++ report.o rate.o -o order_demo && ./order_demo
g++-16 -std=c++23 -c order/rate.cpp -o rate.o
g++-16 -std=c++23 -c order/report.cpp -o report.o
g++-16 report.o rate.o -o order_demo && ./order_demo
clang++ -std=c++23 -O2 -c order/rate.cpp -o rate.o
clang++ -std=c++23 -O2 -c order/report.cpp -o report.o
clang++ report.o rate.o -o order_demo && ./order_demo
""") Output
$ clang++ -std=c++23 -c order/rate.cpp -o rate.o $ clang++ -std=c++23 -c order/report.cpp -o report.o $ clang++ rate.o report.o -o order_demo && ./order_demo base_rate 18, doubled_rate 36 $ clang++ report.o rate.o -o order_demo && ./order_demo base_rate 18, doubled_rate 0 $ g++-16 -std=c++23 -c order/rate.cpp -o rate.o $ g++-16 -std=c++23 -c order/report.cpp -o report.o $ g++-16 report.o rate.o -o order_demo && ./order_demo base_rate 18, doubled_rate 0 $ clang++ -std=c++23 -O2 -c order/rate.cpp -o rate.o $ clang++ -std=c++23 -O2 -c order/report.cpp -o report.o $ clang++ report.o rate.o -o order_demo && ./order_demo base_rate 18, doubled_rate 36
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py
When report.o came first, doubled_rate was computed while base_rate still held its zero, so it became 0, and
nothing reported an error. The last build shows why such bugs come and go: with -O2, Clang worked out that
read_rate() always returns 18 and stored 18 at compile time, which the standard allows, so the same link order
printed 36. A change of optimisation level, compiler or link order can make the bug appear or vanish. This is the
static initialisation order problem: The risk that a global whose first value is set by code at start-up reads a global of another source file before that one's code has run: between source files the standard leaves the order of such initialisation open. More in “C++ const, constexpr, consteval and constinit”, and there are three ways out,
best first.
Avoid globals that depend on other globals. The C++ Core Guidelines advise against complex initialisation of
global objects (I.22) and against non-const global variables in general (R.6).
Wrap the value in a function with a static local, so it is created on first use, whoever asks first:
C++ (rate.cpp) · order_fixed/rate.cpp
// rate.cpp, fixed: the value lives in a static local, created on the first call.
int read_rate() {
return 18;
}
int& base_rate() {
static int rate = read_rate(); // initialised the first time anyone asks, whoever asks first
return rate;
} C++ (report.cpp) · order_fixed/report.cpp
// report.cpp, fixed: asks for the rate through the function.
#include <iostream>
int& base_rate();
int doubled_rate = base_rate() * 2; // calling base_rate() forces its initialisation first
int main() {
std::cout << "base_rate " << base_rate() << ", doubled_rate " << doubled_rate << '\n';
} from session import run
# With a function-local static, both link orders print the same.
run("""
clang++ -std=c++23 -c order_fixed/rate.cpp -o rate.o
clang++ -std=c++23 -c order_fixed/report.cpp -o report.o
clang++ rate.o report.o -o fixed && ./fixed
clang++ report.o rate.o -o fixed && ./fixed
""") Output
$ clang++ -std=c++23 -c order_fixed/rate.cpp -o rate.o $ clang++ -std=c++23 -c order_fixed/report.cpp -o report.o $ clang++ rate.o report.o -o fixed && ./fixed base_rate 18, doubled_rate 36 $ clang++ report.o rate.o -o fixed && ./fixed base_rate 18, doubled_rate 36
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_fixed_session.py
Demand compile-time initialisation with constinit (C++20), when the value can be a constant. A constinit
variable is initialised before any code runs, so no order question arises, and an initialiser that would need run
time becomes a compile error instead of a silent start-up bug:
// constinit_check.cpp: constinit (C++20) demands initialisation at compile time,
// so a global that would depend on start-up order becomes a compile error instead.
int read_rate() { return 18; }
constexpr int fixed_rate() { return 18; }
constinit int safe_rate = fixed_rate(); // fine: a constant expression
constinit int unsafe_rate = read_rate(); // error: read_rate() is not constexpr from session import run
run("""
clang++ -std=c++23 -fsyntax-only constinit_check.cpp
g++-16 -std=c++23 -fsyntax-only constinit_check.cpp
""") Output
$ clang++ -std=c++23 -fsyntax-only constinit_check.cpp
constinit_check.cpp:7:15: error: variable does not have a constant initializer
7 | constinit int unsafe_rate = read_rate(); // error: read_rate() is not constexpr
| ^ ~~~~~~~~~~~
constinit_check.cpp:7:1: note: required by 'constinit' specifier here
7 | constinit int unsafe_rate = read_rate(); // error: read_rate() is not constexpr
| ^~~~~~~~~
constinit_check.cpp:7:29: note: non-constexpr function 'read_rate' cannot be used in a constant expression
7 | constinit int unsafe_rate = read_rate(); // error: read_rate() is not constexpr
| ^
constinit_check.cpp:3:5: note: declared here
3 | int read_rate() { return 18; }
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fsyntax-only constinit_check.cpp
constinit_check.cpp:7:15: error: 'constinit' variable 'unsafe_rate' does not have a constant initializer
7 | constinit int unsafe_rate = read_rate(); // error: read_rate() is not constexpr
| ^~~~~~~~~~~
constinit_check.cpp:7:38: error: call to non-'constexpr' function 'int read_rate()'
7 | constinit int unsafe_rate = read_rate(); // error: read_rate() is not constexpr
| ~~~~~~~~~^~
constinit_check.cpp:3:5: note: 'int read_rate()' declared here
3 | int read_rate() { return 18; }
| ^~~~~~~~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 constinit_session.py
Step through the start-up that printed doubled_rate 0, then both fixes:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Zero first. Before any code runs, all static storage is filled with zeros: base_rate and doubled_rate are both 0.
- report.o runs first. Then the dynamic initialisers run. With report.o first on the link command, doubled_rate = base_rate * 2 runs first and reads base_rate while it is still 0.
- rate.o, too late. Now rate.cpp's initialiser runs: base_rate = read_rate() stores 18. doubled_rate has already been computed and stays 0.
- A silent wrong answer. main prints base_rate 18, doubled_rate 0, and nothing reports an error. With rate.o first the order flips and it prints 36; with Clang -O2, which stored 18 at compile time, it prints 36 even in this link order.
- Fix: a function-local static. base_rate() now returns a static local, initialised on the first call, whoever calls first. doubled_rate's initialiser calls it, so it reads 18 in either order: base_rate 18, doubled_rate 36.
- Or: constinit. constinit demands initialisation at compile time. constinit int unsafe_rate = read_rate(); does not compile with either compiler, because read_rate() is not constexpr: the start-up bug becomes a compile error.
Destruction has the mirror problem: static objects are destroyed in the reverse order of their construction, so a global whose destructor uses another global can find it already destroyed. Keeping destructors of globals simple, or avoiding such globals, prevents it.
Checkpoint
Check: Start-up order of globals
Argument-dependent lookup searches the namespaces of the arguments
A namespace prefixes names: std::vector, shop::billing::Invoice. Since C++17 nested namespaces can be opened in
one line, namespace shop::billing { … }, and a long name can get a short alias:
// namespaces.cpp: nested namespaces, an alias, a using-declaration and argument-dependent lookup.
#include <iostream>
#include <string>
#include <utility>
namespace shop::billing { // C++17 shorthand for namespace shop { namespace billing {
struct Invoice {
std::string id;
long paise;
};
// Found by argument-dependent lookup: it is in the namespace of its argument type.
std::ostream& operator<<(std::ostream& out, const Invoice& inv) {
return out << inv.id << " for " << inv.paise / 100 << '.' << inv.paise % 100 / 10 << inv.paise % 10 << " rupees";
}
void swap(Invoice& a, Invoice& b) noexcept { // a cheaper swap for this type
std::cout << " (billing::swap)\n";
std::swap(a.id, b.id);
std::swap(a.paise, b.paise);
}
long total(const Invoice& a, const Invoice& b) { return a.paise + b.paise; }
}
namespace bill = shop::billing; // an alias for a long name
template <typename T>
void exchange(T& a, T& b) {
using std::swap; // make std::swap a candidate...
swap(a, b); // ...but let argument-dependent lookup find a better one
}
int main() {
bill::Invoice first{"INV-7", 129900};
bill::Invoice second{"INV-8", 4550};
std::cout << first << '\n'; // operator<< found in shop::billing through the argument
std::cout << "total " << total(first, second) << " paise\n"; // no shop::billing:: needed: the arguments name it
exchange(first, second); // picks billing::swap
int x = 1, y = 2;
exchange(x, y); // int has no namespace, so std::swap
std::cout << first << ", then x = " << x << '\n';
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra namespaces.cpp -o namespaces && ./namespaces
g++-16 -std=c++23 -Wall -Wextra namespaces.cpp -o namespaces && ./namespaces
""") Output
$ clang++ -std=c++23 -Wall -Wextra namespaces.cpp -o namespaces && ./namespaces INV-7 for 1299.00 rupees total 134450 paise (billing::swap) INV-8 for 45.50 rupees, then x = 2 $ g++-16 -std=c++23 -Wall -Wextra namespaces.cpp -o namespaces && ./namespaces INV-7 for 1299.00 rupees total 134450 paise (billing::swap) INV-8 for 45.50 rupees, then x = 2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 namespaces_session.py
Two calls in main name no namespace and still work, because of
argument-dependent lookup: The rule that an unqualified function call also searches the namespaces of its argument types, so total(first, second) finds shop::billing::total when the arguments are shop::billing::Invoice. It is also how operator<< is found. (ADL): for an unqualified function call, the
compiler also searches the namespaces of the argument types. total(first, second) finds shop::billing::total
because both arguments are shop::billing::Invoice. The same rule finds the right operator<<: std::cout << first
looks in shop::billing, where the operator for Invoice lives, and std::cout << x for a std::string finds the
operator in std. Without ADL, every operator would need its full name. Step through where each call of main and
exchange() finds its function:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Ordinary lookup. main calls total(first, second) without a namespace. Ordinary lookup searches main's own scopes and the global namespace, and finds no total there.
- Through the arguments. first and second are shop::billing::Invoice objects, so argument-dependent lookup adds shop::billing to the search. It finds total there, and the program prints total 134450 paise.
- Operators too. std::cout << first is a call of operator<< with an Invoice argument, so the same rule finds the operator in shop::billing; for a std::string it finds the one in std.
- Two candidates for swap. In exchange(first, second), using std::swap; makes std::swap a candidate, and the unqualified swap(a, b) lets argument-dependent lookup add billing::swap.
- The better match wins. Both can swap two invoices, and overload resolution prefers the better match, billing::swap: (billing::swap) is printed once, for the invoices.
- An int has no namespace. exchange(x, y) with two ints adds no namespace, so only std::swap is found. std::swap(a, b), written with its namespace, would have skipped billing::swap for the invoices too.
ADL is also why generic code swaps values with two lines: using std::swap; makes the standard version a candidate,
and the unqualified swap(a, b) lets ADL find a better one in the type’s own namespace if there is one. The session
shows exchange() choosing billing::swap for invoices and std::swap for two ints. std::swap(a, b), written
with its namespace, would skip the custom version. Since C++20, std::ranges::swap(a, b) does the two-step lookup
for you.
Checkpoint
Check: Argument-dependent lookup
A using namespace in a header leaks into every file that includes it
using namespace std; is a using-directive: A line such as using namespace std; that makes every name of a namespace usable without its prefix, from that line to the end of the scope it is written in. In a header, it reaches every file that includes the header. More in “C++ program structure, naming and formatting”: it makes every name of std usable
without its prefix in the rest of the scope. In a header, that scope is every file that includes it, which then loses
the protection namespaces give:
C header · ambiguous/util.h
// util.h: a header that brings all of std into the global namespace of every file that includes it.
#ifndef UTIL_H
#define UTIL_H
#include <algorithm>
#include <vector>
using namespace std; // never do this in a header
#endif // UTIL_H C++ · ambiguous/main.cpp
// main.cpp: its own global called count now clashes with std::count from <algorithm>.
#include <iostream>
#include "util.h"
int count = 0;
int main() {
vector<int> marks{40, 75, 40, 90};
count = 3;
std::cout << count << '\n';
} from session import run
# GCC lists every candidate with its library internals, so only its error lines are kept here.
run("""
clang++ -std=c++23 -c ambiguous/main.cpp -o main.o
g++-16 -std=c++23 -c ambiguous/main.cpp -o main.o 2>&1 | grep 'error:'
""") Output
$ clang++ -std=c++23 -c ambiguous/main.cpp -o main.o
ambiguous/main.cpp:9:5: error: reference to 'count' is ambiguous
9 | count = 3;
| ^
ambiguous/main.cpp:5:5: note: candidate found by name lookup is 'count'
5 | int count = 0;
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__algorithm/count.h:86:1: note: candidate found by name lookup is 'std::count'
86 | count(_InputIterator __first, _InputIterator __last, const _Tp& __value) {
| ^
ambiguous/main.cpp:10:18: error: reference to 'count' is ambiguous
10 | std::cout << count << '\n';
| ^
ambiguous/main.cpp:5:5: note: candidate found by name lookup is 'count'
5 | int count = 0;
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__algorithm/count.h:86:1: note: candidate found by name lookup is 'std::count'
86 | count(_InputIterator __first, _InputIterator __last, const _Tp& __value) {
| ^
2 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -c ambiguous/main.cpp -o main.o 2>&1 | grep 'error:'
ambiguous/main.cpp:9:5: error: reference to 'count' is ambiguous
ambiguous/main.cpp:10:18: error: reference to 'count' is ambiguous
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ambiguous_session.py
Step through what main.cpp receives from util.h:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The header opens std. util.h includes <algorithm> and <vector>, then writes using namespace std;. From that line on, every name of std can be used without its prefix.
- Pasted into main.cpp. main.cpp includes util.h, so the using-directive is pasted into main.cpp's global scope too. main.cpp never wrote it, and vector<int> now compiles there without std::.
- A name of its own. main.cpp defines a global variable of its own, int count = 0;, a reasonable name for a file that never asked for std::count.
- Ambiguous. count = 3; is an unqualified name. Lookup finds ::count and, through the using-directive, std::count, and neither is better: both compilers stop with reference to count is ambiguous.
- The fix. Delete the using-directive from the header, as the C++ Core Guidelines rule SF.7 says. In a .cpp file, after every #include, a using-declaration such as using std::vector; brings in only the names you need.
main.cpp never wrote using namespace std;, yet its own count now clashes with std::count. The error appears
in a file that is correct by itself, often far from the header that caused it, and every new name added to std by a
later standard can cause another one. The C++ Core Guidelines rule is direct: no using namespace at global scope in a header (SF.7). In a .cpp file, after every
#include, it affects that file only; even there, a using-declaration of the names you need, such as
using std::vector;, keeps the rest of std out of the way.
Checkpoint
Check: using namespace in headers
Practise: untangle two files
This program has three source files. inventory.cpp and orders.cpp each define their own count and note(),
and the program does not link:
C++ (inventory.cpp) · store/inventory.cpp
// inventory.cpp
#include <iostream>
int count = 0; // units in stock
void note(const char* what, int n) { std::cout << "[" << what << "] " << n << '\n'; }
void add_stock(int n) {
count += n;
note("stock", n);
}
int in_stock() { return count; } C++ (orders.cpp) · store/orders.cpp
// orders.cpp
#include <iostream>
int count = 0; // units ordered
void note(const char* what, int n) { std::cout << "[" << what << "] " << n << '\n'; }
void place_order(int n) {
count += n;
note("order", n);
}
int ordered() { return count; } C++ (main.cpp) · store/main.cpp
// main.cpp
#include <iostream>
void add_stock(int n);
int in_stock();
void place_order(int n);
int ordered();
int main() {
add_stock(5);
add_stock(3);
place_order(2);
std::cout << "in stock: " << in_stock() << ", ordered: " << ordered() << '\n';
} Make it link and print the four lines shown below, without renaming count or note:
- Give each file’s
countandnote()internal linkage, preferably with an unnamed namespace. - Put the public functions into namespaces,
store::inventoryandstore::orders, declared in one header,store.h, with an include guard. - In
main.cpp, include the header and call the functions through their namespaces; try a namespace alias. - Check with
nmthatcountandnoteappear only with lower-case letters.
from session import run
# The starting program does not link. One possible fix does, with both compilers.
run("""
clang++ -std=c++23 -c store/inventory.cpp -o inventory.o
clang++ -std=c++23 -c store/orders.cpp -o orders.o
clang++ -std=c++23 -c store/main.cpp -o main.o
clang++ inventory.o orders.o main.o -o store_demo
clang++ -std=c++23 -Wall -Wextra store_solution/inventory.cpp store_solution/orders.cpp store_solution/main.cpp -o fixed && ./fixed
g++-16 -std=c++23 -Wall -Wextra store_solution/inventory.cpp store_solution/orders.cpp store_solution/main.cpp -o fixed && ./fixed
""") Output
$ clang++ -std=c++23 -c store/inventory.cpp -o inventory.o
$ clang++ -std=c++23 -c store/orders.cpp -o orders.o
$ clang++ -std=c++23 -c store/main.cpp -o main.o
$ clang++ inventory.o orders.o main.o -o store_demo
duplicate symbol 'note(char const*, int)' in:
orders.o
inventory.o
duplicate symbol '_count' in:
orders.o
inventory.o
ld: 2 duplicate symbols
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
[exit status 1]
$ clang++ -std=c++23 -Wall -Wextra store_solution/inventory.cpp store_solution/orders.cpp store_solution/main.cpp -o fixed && ./fixed
[stock] 5
[stock] 3
[order] 2
in stock: 8, ordered: 2
$ g++-16 -std=c++23 -Wall -Wextra store_solution/inventory.cpp store_solution/orders.cpp store_solution/main.cpp -o fixed && ./fixed
[stock] 5
[stock] 3
[order] 2
in stock: 8, ordered: 2
This output changes from run to run: the linker may list the object files, and the duplicate names, in either order
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 store_session.py
C header · store_solution/store.h
// store.h: the public names, grouped into namespaces.
#ifndef STORE_H
#define STORE_H
namespace store::inventory {
void add_stock(int n);
int in_stock();
}
namespace store::orders {
void place_order(int n);
int ordered();
}
#endif // STORE_H C++ (inventory.cpp) · store_solution/inventory.cpp
// inventory.cpp: private names in an unnamed namespace, public ones in store::inventory.
#include <iostream>
#include "store.h"
namespace {
int count = 0; // units in stock, visible in this file only
void note(const char* what, int n) { std::cout << "[" << what << "] " << n << '\n'; }
}
namespace store::inventory {
void add_stock(int n) {
count += n;
note("stock", n);
}
int in_stock() { return count; }
} C++ (orders.cpp) · store_solution/orders.cpp
// orders.cpp: the same pattern; its count and note() cannot clash with inventory.cpp's.
#include <iostream>
#include "store.h"
namespace {
int count = 0; // units ordered
void note(const char* what, int n) { std::cout << "[" << what << "] " << n << '\n'; }
}
namespace store::orders {
void place_order(int n) {
count += n;
note("order", n);
}
int ordered() { return count; }
} C++ (main.cpp) · store_solution/main.cpp
// main.cpp: uses the public names through the header.
#include <iostream>
#include "store.h"
int main() {
namespace inv = store::inventory;
inv::add_stock(5);
inv::add_stock(3);
store::orders::place_order(2);
std::cout << "in stock: " << inv::in_stock() << ", ordered: " << store::orders::ordered() << '\n';
} Both compilers link the solution and print the same four lines, 8 in stock and 2 ordered. Then practise the seven ideas of this lesson; the questions start where your answers so far are weakest, and each wrong choice explains itself:
Practice that adapts to you
Practise this lesson
15 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 variable’s scope and its lifetime? Give an example where they differ.
Scope is a property of the name in the source code: where you can write it. Lifetime, set by the storage duration, is
a property of the object at run time: when it exists. For an ordinary local variable they match: the name is usable
until the closing brace, and the object is destroyed there. A static local is the classic difference: static int issued = 0; inside next_ticket() has block scope, so no other function can name it, but static storage duration,
so the object is created on the first call and lives until the program ends, keeping its value between calls. The
reverse also happens: a dangling reference is a name still in scope whose object has already died.
An interviewer may take the answer further, to threads and to the reverse case. Decide each follow-up before you read the note on every option:
Checkpoint
Interview round: two follow-up questions
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A header defines static int requests_served = 0; and an inline count_request() that increments it. Twelve .cpp files include the header, and the monitoring page, which reads requests_served in main.cpp, shows far fewer requests than were served. Explain what the program contains, and fix it.
-
Mid to senior
Why does generic code write using std::swap; followed by swap(a, b); instead of std::swap(a, b)? What breaks with each alternative, and what does std::ranges::swap change?
Key takeaways
- Scope is where a name can be written, storage duration is how long the object lives, and linkage is whether other
files can refer to the name; they are independent, and
staticchanges a different one depending on where it is written. - Locals die at their closing brace and statics after
mainreturns, in the reverse order of their creation. - A static local is created on first use, once, even with many threads;
thread_localgives one object per thread. - Names at namespace scope have external linkage; an unnamed namespace or
staticmakes them private to the file. - Globals in different files initialise in no specified order, and the bug can depend on the link order and the
optimiser: use a function-local static,
constinit, or no such global. - Argument-dependent lookup searches the arguments’ namespaces, which is why operators work unqualified and why
generic code writes
using std::swap; swap(a, b);; never putusing namespacein a header.
Terms in this lesson
- argument-dependent lookup
- The rule that an unqualified function call also searches the namespaces of its argument types, so total(first, second) finds shop::billing::total when the arguments are shop::billing::Invoice. It is also how operator<< is found.
- 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.
- linkage
- Whether other translation units can refer to a name: external linkage means every file can, internal linkage means the name is private to its file, and no linkage means it is a local name.
- storage duration
- How long an object exists while a program runs: automatic (until the end of its block), static (the whole run), thread (the life of one thread) or dynamic (from new until delete). It belongs to the object, while scope belongs to its name.
- unnamed namespace
- A namespace written without a name, namespace { … }. Everything declared in it has internal linkage, so it is private to its file; unlike static, it works for types too.
References
- Working Draft, Programming Languages: C++ (N4950), 6.4 Scope, 6.6 Program and linkage, 6.7.5 Storage duration, 6.9.3 Start and termination, 8.8 Declaration statement, 9.8 Namespaces (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines (I.22, R.6, SF.7, SF.21, SF.22) (Standard C++ Foundation)
- ThreadSanitizer (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress