Lesson 4 of 10
Every name you declare at namespace scope has a linkage: whether other translation units can refer to it. Get it wrong and you get either a link error or — worse — two objects where you meant one, which lesson 17.2 already showed silently losing data.
There are only three answers, and nm will tell you which one you got.
The whole table, in one object file
// link.cpp
int external_thing = 1; // external linkage
static int internal_thing = 2; // internal linkage
const int const_thing = 3; // const at namespace scope: INTERNAL
extern const int extern_const_thing = 4; // const + extern: external again
inline int inline_thing = 5; // external, exactly one object
constexpr int constexpr_thing = 6; // implicitly const, so internal
namespace {
int anon_thing = 7; // internal linkage
}
int sum() {
return external_thing + internal_thing + const_thing + extern_const_thing
+ inline_thing + constexpr_thing + anon_thing;
}
g++ -std=c++20 -Wall -Wextra -c link.cpp -o link.o
nm -C link.o | grep -v ' U ' | sort -k3
0000000000000008 d (anonymous namespace)::anon_thing
0000000000000000 r const_thing
0000000000000008 r constexpr_thing
0000000000000004 R extern_const_thing
0000000000000000 D external_thing
0000000000000000 u inline_thing
0000000000000004 d internal_thing
0000000000000000 T sum()
The rule for reading that output is simple and worth memorising:
An uppercase letter means the symbol is visible to other object files. A lowercase letter means it is private to this one.
D/d— initialised dataR/r— read-only dataT/t— codeB/b— zero-initialised datau— a unique global object, which is whatinlinevariables getU— undefined: this file uses it and expects someone else to define it
The surprising row
const int const_thing = 3; is internal. A const at namespace scope has internal linkage by
default in C++, unlike every other kind of variable, and unlike C.
That is why putting const int kMax = 100; in a header is harmless — each translation unit gets its
own private copy, and since it is const, nobody can tell. It is also why the static trap of lesson
17.2 only bites non-const variables.
If you genuinely want one shared constant object — because you take its address, or it is expensive
to construct — say extern const and define it in one .cpp, or use inline constexpr in the
header. In modern C++ inline constexpr is almost always the right answer.
extern: declaring something defined elsewhere
For functions, a declaration needs no keyword. For variables it does, because otherwise it is a definition:
// counters.hpp
#pragma once
extern int hits; // declaration: defined in exactly one .cpp
// int hits; // this would be a DEFINITION, one per including file
// counters.cpp
#include "counters.hpp"
int hits = 0; // the one definition
// main.cpp
#include <iostream>
#include "counters.hpp"
int main() {
hits += 3;
std::cout << "hits = " << hits << "\n";
}
g++ -std=c++20 -c counters.cpp -o counters.o
g++ -std=c++20 -c main.cpp -o main.o
g++ counters.o main.o -o prog && ./prog
echo "--- how each file sees 'hits':"
nm -C main.o | grep hits
nm -C counters.o | grep hits
hits = 3
--- how each file sees 'hits':
U hits
0000000000000000 B hits
U in one file, D in the other. That is the shape of every correctly shared global.
Anonymous namespaces: the modern static
An unnamed namespace gives internal linkage to everything inside it:
// service.cpp
#include <string>
namespace {
// Helpers nobody outside this file should be able to call, or even collide
// with. An anonymous namespace is the C++ way to say so.
std::string normalise(std::string s) {
for (char& c : s) c = (c == ' ') ? '_' : c;
return s;
}
constexpr int kRetryLimit = 3;
} // namespace
std::string service_key(const std::string& raw) {
return normalise(raw) + "/" + std::to_string(kRetryLimit);
}
Prefer it to static for anything file-local, for two reasons. It works on types, which static
does not — and a class defined in a .cpp without one has external linkage, so two files each
defining a local struct Helper differently is an ODR violation of exactly the kind lesson 17.2
demonstrated. And it groups: one namespace { ... } block marks a whole section as private, instead
of static repeated on twelve declarations.
The static initialisation order fiasco
Globals with external linkage bring a problem that has its own name.
Within one translation unit, globals are initialised in declaration order. Across translation units, the order is unspecified — and in practice it is the order the object files were given to the linker.
// config.hpp
#pragma once
#include <string>
struct Config {
std::string name;
int port;
Config(std::string n, int p);
};
extern Config settings;
// config.cpp
#include <iostream>
#include "config.hpp"
// Dynamic initialisation: the constructor must run before this is usable.
Config settings("production", 8080);
Config::Config(std::string n, int p) : name(std::move(n)), port(p) {
std::cout << " [init] Config constructed: " << name << ":" << port << "\n";
}
// client.cpp
#include <iostream>
#include "config.hpp"
struct Client {
Client() {
std::cout << " [init] Client reading settings: '" << settings.name
<< "' port " << settings.port << "\n";
}
};
Client the_client;
// main.cpp
#include <iostream>
#include "config.hpp"
int main() {
std::cout << "main: " << settings.name << ":" << settings.port << "\n";
return 0;
}
Same source, same compiler, same flags. The only difference is the order of two arguments:
for f in config client main; do g++ -std=c++20 -Wall -Wextra -c $f.cpp -o $f.o; done
echo "=== link order A: client.o before config.o"
g++ client.o config.o main.o -o a && ./a
echo "=== link order B: config.o before client.o"
g++ config.o client.o main.o -o b && ./b
=== link order A: client.o before config.o
[init] Client reading settings: '' port 0
[init] Config constructed: production:8080
main: production:8080
=== link order B: config.o before client.o
[init] Config constructed: production:8080
[init] Client reading settings: 'production' port 8080
main: production:8080
In order A, Client read settings before Config's constructor ran: an empty name and port
zero. The object existed — static storage is zero-initialised before anything else happens — but it
was not yet a Config.
Nothing warned. Both programs exit 0. And note that main prints the right answer in both cases,
because by the time main runs, all initialisation is done. The bug lives entirely in the window
before main, which is why it so often shows up as "it works in the test binary and not in the real
one" — the two link their objects in a different order.
The fix: a function-local static
Move the object inside a function and return a reference. A function-local static is initialised on first call, so ordering is determined by use rather than by the linker:
// config.hpp
#pragma once
#include <string>
struct Config {
std::string name;
int port;
Config(std::string n, int p);
};
// Constructed on the FIRST CALL, not before main. Order is now determined by
// use, not by link order.
Config& settings();
// config.cpp
#include <iostream>
#include "config.hpp"
Config::Config(std::string n, int p) : name(std::move(n)), port(p) {
std::cout << " [init] Config constructed: " << name << ":" << port << "\n";
}
Config& settings() {
static Config instance("production", 8080);
return instance;
}
// client.cpp
#include <iostream>
#include "config.hpp"
struct Client {
Client() {
std::cout << " [init] Client reading settings: '" << settings().name
<< "' port " << settings().port << "\n";
}
};
Client the_client;
// main.cpp
#include <iostream>
#include "config.hpp"
int main() {
std::cout << "main: " << settings().name << ":" << settings().port << "\n";
return 0;
}
for f in config client main; do g++ -std=c++20 -Wall -Wextra -c $f.cpp -o $f.o; done
echo "=== link order A:"
g++ client.o config.o main.o -o a && ./a
echo "=== link order B:"
g++ config.o client.o main.o -o b && ./b
=== link order A:
[init] Client reading settings: ' [init] Config constructed: production:8080
production' port 8080
main: production:8080
=== link order B:
[init] Client reading settings: ' [init] Config constructed: production:8080
production' port 8080
main: production:8080
Both orders now produce identical output, and Client always sees a fully constructed Config.
The interleaving in that output is not a glitch — it is the mechanism made visible. settings() is
called in the middle of Client's output expression, so the [init] Config constructed line lands
inside the half-written Client line. The construction is happening exactly when it is first
needed, which is the entire point.
Two further properties worth knowing: since C++11, function-local static initialisation is thread-safe — the compiler emits a guard so concurrent first calls block rather than race (Module 20 returns to this). And the object is destroyed in reverse order of construction at exit, which is the same ordering problem again, on the way out. If a destructor at shutdown touches another singleton, you can have a destruction order fiasco; the usual answer is to leak deliberately rather than destroy.
Try it
Build a four-file program in which each file declares a namespace-scope name of every linkage kind,
then use nm to produce a table of which names each object file exports and which it imports.
Deliberately create a collision between two file-local helpers — once with static, once without
— and explain the difference in outcome.
Checkpoint
- Every namespace-scope name has external or internal linkage;
nm's case tells you which. - Uppercase = visible to other object files, lowercase = private to this one.
U= needed from elsewhere. staticat namespace scope, and anonymous namespaces, give internal linkage.constat namespace scope is internal by default — unlike C, and unlike everything else.- For one shared constant object, use
inline constexprin the header, orextern const. externon a variable makes it a declaration; without it, a header definition is one per file.- Prefer an anonymous namespace to
static: it works on types, and it groups. - A
structdefined in a.cpphas external linkage — two files disagreeing about one is an ODR violation, andstaticcannot prevent it. - Globals initialise in declaration order within a TU, and in unspecified order across TUs.
- Measured: swapping two arguments to the linker made a constructor read an object before it was constructed — empty string, port 0, no warning, exit 0.
- The bug lives entirely before
main, which is why it appears in one binary and not another. - Function-local statics are initialised on first call, which makes order a function of use.
- They are thread-safe since C++11, and destroyed in reverse order at exit — which can produce the same fiasco on shutdown.
Questions about this lesson
Log in to ask a question.