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

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.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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

  1. Question 1 of 2 What does static change in static int issued = 0; written inside a function?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its storage duration: the object lives from the first call to the end of the program, while the name keeps its block scope

    The keyword means two things. Inside a function it gives static storage duration to a block-scope name; at namespace scope, where storage is static anyway, it gives the name internal linkage.

  2. Question 2 of 2 Which linkage does const int max_retries = 3; have at namespace scope, and what does that mean when the line is in a header?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Internal, so every file that includes the header gets a private copy of its own

    The table's fifth row: namespace scope, static storage, internal linkage. Each translation unit that includes the header holds its own max_retries, which is harmless for a small constant.

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 C++ · lifetimes.cpp
// 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
One run, every creation and destruction Python · lifetimes_session.py
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

A timeline: globals are created before main, locals live in their block, a static local is created on first use, statics die at exit.Program startsstatic storage is zero-filled,then globals are constructedmain() beginsA block runsits local is created at its declarationand destroyed at the closing braceFirst call of visit()the static local is created once;the ordinary local is created and destroyedSecond call of visit()only the ordinary local is created and destroyedmain() returnsProgram endsstatic objects are destroyed in reverse order:the static local first, then the global

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.

  1. The program starts. All static storage is first filled with zeros, then the global object is constructed, before main() begins.
  2. main() begins.
  3. A block runs. Its local object is created when its declaration is reached and destroyed at the closing brace of the block.
  4. The first call of visit() creates the function's static local object, once, and also creates and destroys the function's ordinary local object.
  5. The second call of visit() creates and destroys only the ordinary local object; the static local object already exists.
  6. main() returns.
  7. 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:

Interactive · step by step Which objects of lifetimes.cpp exist, line by line of its output Step through the printed lines of one run. Each box is one Tracer; its sub-label says whether it exists at that moment.
Which objects of lifetimes.cpp exist, line by line of its output, step 7 of 7. Showing global (destroyed last), block (in main) (destroyed at the brace), once (static local) (destroyed after main), each (local of visit) (created and destroyed again). In focus: once (static local), global.globaldestroyed lastblock (in main)destroyed at the braceonce (static local)destroyed after maineach (local of visit)created and destroyed again

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

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. End of the first call. At the end of visit(), each is destroyed. once is not: a static local lives until the program ends.
  6. 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.
  7. 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 main printed anything, and destroyed after main returned.
  • 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

  1. Question 1 of 2 After main returns in the lifetimes run, which object is destroyed first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The static local of visit(), because statics die in the reverse order of their creation and it was created last

    Objects with static storage duration are destroyed in the reverse order of the completion of their construction: destroy static local in visit() comes before destroy global (static storage).

  2. Question 2 of 2 How many lines does the second call of visit() print in the lifetimes run?

    Type a number.

    A hint

    Which objects does the second call create?

    Show the answer: Question 2 of 2

    Answer: 2 lines

    Only the ordinary local is created, create local in visit(), and destroyed, destroy local in visit(): 2 lines. The static local was created on the first call and is not created again.

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 C++ · counter.cpp
// 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 C++ · once_threads.cpp
// 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';
}
A counter, then eight threads and one construction Python · counter_session.py
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:

Interactive · message by message Eight threads, one static Table Step through the race. One thread arrives first and builds the table; watch what the other seven do meanwhile.
Eight threads, one static Table, step 5 of 5. Messages so far: First thread to static Table table: runs the constructor; Other seven threads to static Table table: wait for it; static Table table to First thread: the table; static Table table to Other seven threads: the same table.First threadreaches it firstOther seven threadsarrive meanwhilestatic Table tablebuilt once, 100 rowsruns the constructorwait for itthe tablethe same table

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

Every step as text
  1. 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.
  2. 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.
  3. Built once. The constructor returns. The table now exists with its 100 rows, and the first thread gets a reference to it.
  4. Everyone shares it. The waiting threads go on with the same object. Nobody runs the constructor a second time.
  5. 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 C++ · per_thread.cpp
// 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';
}
Two threads, three counters, one shared total Python · per_thread_session.py
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

  1. Question 1 of 2 next_ticket() declares static int issued = 0; and returns ++issued. What does its fifth call return?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 5

    The initialisation runs once, on the first call. Each call then adds 1 to the same object, so the calls return 1, 2, 3, 4 and 5. not_a_counter(), with an ordinary local, would return 1 every time.

  2. Question 2 of 2 In per_thread.cpp, why did main report 0 for calls_in_this_thread?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: main has its own copy of the thread_local variable, and main never called work()

    thread_local gives one object per thread, which lives as long as its thread: thread 1 counted 3, thread 2 counted 5, main counted 0, and the ordinary atomic global saw all 8.

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:

Two files, the same private names

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';
}
The fix: internal linkage, two ways

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';
}
Duplicate symbols, then two private copies Python · collide_session.py
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:

Interactive · step by step Two files, the same private names Step through the link of payments.o and refunds.o, before and after the fix. Watch which names the linker can see.
Two files, the same private names, step 5 of 5. Showing payments.cpp (internal: in namespace { }), refunds.cpp (internal: static), record_payment() (external: T record_payment), Linker (sees no private names). In focus: record_payment().refunds.cpp calls itpayments.cppinternal: in namespace { }refunds.cppinternal: staticrecord_payment()external: T record_paymentLinkersees no private names

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

Every step as text
  1. 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.
  2. Two of each. The linker finds two definitions of each name: duplicate symbol _attempts and duplicate symbol valid_amount(long), 2 duplicate symbols.
  3. 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().
  4. 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.
  5. 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

  1. Question 1 of 2 After the fix, nm prints b (anonymous namespace)::attempts for payments.o. What does the lower-case b tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: attempts is zero-initialised data that is local to payments.o, and the linker never matches it across files

    nm marks external symbols with upper-case letters (T record_payment) and local ones with lower-case letters (t for code, b for zero-initialised data). Internal linkage is what makes a symbol local.

  2. Question 2 of 2 Which declarations at namespace scope of a .cpp file keep the function helper invisible to other files?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • static int helper(int x);
    • namespace { int helper(int x); }

    Internal linkage comes from static or an unnamed namespace. The C++ Core Guidelines prefer the unnamed namespace, because it also works for types.

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:

A global initialised from a global in another file

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';
}
Two link orders, two compilers, -O0 and -O2 Python · order_session.py
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:

The value in a function-local static

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';
}
Both link orders, the same result Python · order_fixed_session.py
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 C++ · constinit_check.cpp
// 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
constinit rejects a run-time initialiser Python · constinit_session.py
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:

Interactive · step by step base_rate and doubled_rate at start-up, report.o first Step through the start of the program linked as report.o rate.o, then the fixes. Watch the values of the two globals.
base_rate and doubled_rate at start-up, report.o first, step 6 of 6. Showing base_rate (static local in base_rate()), doubled_rate (18 * 2 = 36), read_rate(), main prints (base_rate 18, doubled_rate 36). In focus: read_rate().* 2initialisesbase_ratestatic local in base_rate()doubled_rate18 * 2 = 36read_rate()returns 18main printsbase_rate 18, doubled_rate 36error: not a constant initialiser

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

Every step as text
  1. Zero first. Before any code runs, all static storage is filled with zeros: base_rate and doubled_rate are both 0.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 Linked as report.o rate.o, the program printed doubled_rate 0. Why exactly 0, and not some leftover value?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Static storage is filled with zeros before any dynamic initialiser runs, so base_rate was 0 when doubled_rate was computed

    Before main, every variable with static storage is first zero-filled (or set to a constant the compiler can compute), then the dynamic initialisers run, in an unspecified order across files.

  2. Question 2 of 2 What does constinit do to constinit int x = read_rate(); when read_rate() is an ordinary function?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It turns the start-up order risk into a compile error, because the initialiser is not a constant expression

    constinit (C++20) requires a constant initialiser, so the variable has its value before any code runs and no order question can arise. A value that needs run-time code needs a function-local static instead.

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 C++ · namespaces.cpp
// 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';
}
Aliases, operators and swap found through the arguments Python · namespaces_session.py
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:

Interactive · step by step Where the compiler finds total and swap Step through the calls in main and in exchange(). Watch which namespaces each call searches.
Where the compiler finds total and swap, step 6 of 6. Showing total(first, second) (swap(a, b) for two ints), main's scopes, shop::billing, std. In focus: std.ordinary lookupthrough the argumentsusing std::swap;total(first, second)swap(a, b) for two intsmain's scopesno total declared hereshop::billingInvoice, total, swap, <<stdstd::swap, string's <<

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

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

  1. Question 1 of 2 shop::billing also declares void print(const Invoice&);. Which call in main compiles without writing a namespace?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: print(first), where first is a shop::billing::Invoice

    For an unqualified call, the compiler also searches the namespaces of the argument types. That is how total(first, second) found shop::billing::total.

  2. Question 2 of 2 Generic code writes std::swap(a, b) where exchange() wrote using std::swap; swap(a, b);. What changes for two Invoice objects?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: std::swap is called and billing::swap is skipped, so the type's own cheaper swap is lost

    The two-line form makes std::swap a fallback and lets ADL find a better swap in the type's namespace. Since C++20, std::ranges::swap(a, b) does both steps for you.

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:

A header that opens std for every file

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';
}
A global called count meets std::count Python · ambiguous_session.py
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:

Interactive · step by step A using-directive that travels with its header Step through what main.cpp gets from util.h, and why its own count stops compiling.
A using-directive that travels with its header, step 5 of 5. Showing util.h (no using-directive), main.cpp, std::count, ::count. In focus: util.h.#includevisible unqualifiedvisible unqualifiedutil.hno using-directivemain.cppincludes util.hstd::countfrom <algorithm>::countmain.cpp's int count = 0;

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

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

  1. Question 1 of 2 Why did main.cpp's own global count stop compiling?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The using namespace std; of util.h made std::count usable unqualified in main.cpp too, so count = 3; was ambiguous

    A using-directive in a header is pasted into every file that includes it, so those files lose the protection of the std:: prefix without ever having asked for it.

  2. Question 2 of 2 Which change fixes the build and keeps util.h safe for every file that includes it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Remove using namespace std; from util.h and write the std:: prefix where it is needed

    No using namespace at global scope in a header. In a .cpp file, after every #include, a using-declaration such as using std::vector; brings in only the names you need.

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:

The starting program

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:

  1. Give each file’s count and note() internal linkage, preferably with an unnamed namespace.
  2. Put the public functions into namespaces, store::inventory and store::orders, declared in one header, store.h, with an include guard.
  3. In main.cpp, include the header and call the functions through their namespaces; try a namespace alias.
  4. Check with nm that count and note appear only with lower-case letters.
The starting program, then one possible solution Python · store_session.py
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

One possible solution

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.

  1. Question 1 of 15 Which property says whether another .cpp file can refer to a name?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: Its linkage

    Scope is where a name can be written, storage duration is how long its object lives, and linkage is whether other translation units can refer to the name.

  2. Question 2 of 15 static int issued = 0; is declared inside the function next_ticket(). Which statement is true?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: Only next_ticket() can name it, and it keeps its value from one call to the next until the program ends.

    Inside a function, static changes the storage duration, not the scope. The name has block scope and no linkage, so no other function or file can refer to it, but the object is created on the first call and lives until the program ends, which is why the counter session printed tickets 1, 2 and 3.

  3. Question 3 of 15 When is the global Tracer of lifetimes.cpp created?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Before main starts

    An object with static storage duration at namespace scope is built at start-up, before main, and destroyed after main returns.

  4. Question 4 of 15 If main of lifetimes.cpp called visit() three times instead of twice, how many lines starting with create would the run print?

    Type a number.

    A hint

    Count the global, the block's local, the static local and one local per call.

    Show the answer: Question 4 of 15

    Answer: 6 lines

    global 1, block 1, the static local once 1, and the ordinary local of visit() once per call, 3: that is 6. The run with two calls printed 5.

  5. Question 5 of 15 One run of per_thread.cpp has three threads: main and the two jthreads. How many separate objects named calls_in_this_thread does the run have in total?

    Type a number.

    Show the answer: Question 5 of 15

    Answer: 3 objects

    A thread_local variable has a distinct object for each thread, which lives as long as that thread: one for each of the two worker threads and one for main, which is why main's copy printed 0 while the others counted 3 and 5. The std::atomic total is an ordinary global, so there is exactly one of it.

  6. Question 6 of 15 The constructor of a static local throws an exception on the first call. What happens on the next call?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: The initialisation is tried again, because the variable was never initialised

    If the initialiser throws, the variable is not initialised, and the next call tries again. Recursion back into the same initialisation, before it finishes, is undefined behaviour.

  7. Question 7 of 15 Eight threads share the Table that shared_table() returns, and each now adds a row to it. Is that safe?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: No: only the initialisation is synchronised, so the additions race; guard them with a mutex

    A function-local static gives a safe first construction. Shared changes still need a mutex, or an atomic for a single value.

  8. Question 8 of 15 What does putting int attempts = 0; inside namespace { … } in payments.cpp change?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: attempts gets internal linkage, so another file can define its own attempts without a clash.

    Everything in an unnamed namespace has internal linkage: it is visible throughout its own file and invisible to the linker from other files, as the lower-case b that nm printed for attempts shows. Its storage duration stays static.

  9. Question 9 of 15 Two .cpp files each define a struct Row with different members, for their own private use. How do you keep the program correct?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: Put each file's Row in an unnamed namespace

    An unnamed namespace works for everything a file keeps to itself, types included, which is why the C++ Core Guidelines recommend it for all entities internal to a file (SF.22).

  10. Question 10 of 15 report.cpp initialises a global from a global in rate.cpp, and the result depends on the link order. Which change makes every link order give the same result?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: Return the value from a function that holds it in a static local, and call that function.

    A function-local static is initialised the first time the function is called, so whoever asks first gets an initialised value. A fixed link order works only until someone changes the build, and the -O2 result was the compiler choosing to compute the value at compile time, which no other build is required to repeat.

  11. Question 11 of 15 Two globals in the same .cpp file both have dynamic initialisers. In which order do they run?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: In the order of their definitions in the file

    Dynamic initialisers run in definition order within a file; across files, the standard specifies no order, which is what makes the start-up order problem.

  12. Question 12 of 15 In namespaces.cpp, why does total(first, second) compile in main() without writing shop::billing::?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The arguments are shop::billing::Invoice objects, so argument-dependent lookup also searches shop::billing.

    For an unqualified function call, the compiler adds the namespaces of the argument types to the search. That is the same rule that finds operator<< for Invoice in std::cout << first. An alias only shortens a name: you would still have to write bill::total.

  13. Question 13 of 15 Which call does the two-step swap lookup for you, since C++20?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: std::ranges::swap(a, b)

    Before C++20, generic code wrote using std::swap; swap(a, b);. std::ranges::swap does the same in one call.

  14. Question 14 of 15 Which line brings only std::vector into the rest of a .cpp file?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: using std::vector;

    A using-declaration brings in one name; a using-directive brings in a whole namespace. In a .cpp file, after every #include, the first keeps the rest of std out of the way.

  15. Question 15 of 15 Where may using namespace std; appear without affecting any other file?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: In a .cpp file, after every #include

    The C++ Core Guidelines rule SF.7 forbids using namespace at global scope in a header. In a .cpp file, the directive affects that file only.

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

  1. Question 1 of 2 The interviewer goes on: thread_local int calls_in_this_thread = 0; at namespace scope. What are its scope and its lifetime?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Namespace scope, usable from its declaration on; one object per thread, which lives as long as that thread

    thread_local changes the storage duration only: the name keeps its scope and its linkage, and each thread gets an object that lives as long as the thread.

  2. Question 2 of 2 Then: show the reverse case, a name still in scope whose object has already died.

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A function returns a reference to its own local variable; the caller's reference is in scope, but the local died when the function returned

    Scope belongs to names and lifetime to objects, so they can come apart in both directions: a static local's object outlives its name's scope, and a dangling reference's name outlives its object.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

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

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

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 static changes a different one depending on where it is written.
  • Locals die at their closing brace and statics after main returns, in the reverse order of their creation.
  • A static local is created on first use, once, even with many threads; thread_local gives one object per thread.
  • Names at namespace scope have external linkage; an unnamed namespace or static makes 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 put using namespace in 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

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.