Ch25.3: Storage, Linkage and Namespaces
1. Three Different Questions
"Where does this name live?" is actually three questions, and confusing them causes most of the bugs in this area:
| Question | Term | Answer |
|---|---|---|
| Where can the name be used in the source? | Scope | Block, class, namespace, function parameter, and so on |
| How long does the object live? | Storage duration | Automatic, static, thread, or dynamic |
| Can another translation unit refer to it? | Linkage | No linkage, internal linkage, or external linkage |
A variable can have block scope and static storage duration (a local
static). It can have namespace scope and internal linkage (an anonymous
namespace). It can have namespace scope and external linkage (an ordinary global). The three
properties are independent.
2. Local static: One Object, Initialized Once
int next_id()
{
static int counter{};
return ++counter;
}
counter is a block-scope variable with static storage duration. There is one
object per program, not one per call. It is initialized the first time control reaches the
declaration, and destroyed when the program exits.
Since C++11, the initialization is thread-safe: if two threads call next_id for
the first time at once, one waits for the other, and the object is constructed exactly once.
That guarantee is about initialization, not about later access. The
++counter itself is a data race if two threads call it concurrently. Use
std::atomic<int> if that is what you mean.
#include <atomic>
int next_id()
{
static std::atomic<int> counter{};
return ++counter;
}
Local statics are good for lazy singletons, caches and one-time initialization. They are bad for hidden global state: the function signature no longer tells the reader that calls share data. If the state must be reset for a test, a local static makes that impossible without a separate reset hook.
3. thread_local: One Object Per Thread
thread_local int per_thread_counter{};
void bump()
{
++per_thread_counter;
}
Each thread gets its own per_thread_counter. The object is created when the
thread first reaches the declaration, and destroyed when the thread exits. It is the storage
duration counterpart of a thread-local cache: no locking, no sharing, no false sharing.
In a header, use inline thread_local so every translation unit refers to the
same entity:
inline thread_local int per_thread_counter{};
thread_local has costs. A dynamic initializer runs per thread, the object may
need a TLS slot, and the destructor runs during thread exit. On a freestanding target there
may be no thread-local storage at all. Do not use it for a value that is logically global;
use it when the value is genuinely per-thread.
A thread_local variable is not automatically synchronized with other threads.
It is private state, not a communication channel. If two threads must see the same value, use
an atomic or a mutex-protected object, not thread_local.
4. inline Variables
Before C++17, a header could not define a global variable: every translation unit that
included the header would define it, and the linker would report multiple definitions. The
workarounds were a static variable per translation unit (one copy each) or an
extern declaration plus one definition in a .cc file.
C++17 added inline variables. An inline variable has external linkage and one
definition across all translation units, even though the definition appears in a header:
inline constexpr int protocol_version = 3;
inline std::string default_name{"fast_io"};
inline here is about linkage and the ODR, not about inlining the variable's
address or avoiding a memory load. The compiler may still place the object in one location
and refer to it from every translation unit.
Use inline constexpr for header-defined constants and
inline for header-defined objects that need one identity. A non-trivial inline
variable has dynamic initialization; its initialization order relative to other translation
units is still a problem. Prefer constant initialization or a function-local static when
order matters.
5. Namespace-Scope const Is Special
A namespace-scope const variable has internal linkage by default in C++:
const int limit = 100; // internal linkage
extern const int shared = 100; // external linkage, defined here
inline constexpr int version = 3; // one entity across TUs
That is why a header with const int limit = 100; does not cause a linker error:
each translation unit gets its own copy. It is also why taking the address of
limit in two translation units may produce two different addresses. If you need
one identity, write inline constexpr or extern const with one
definition.
The rule is a C++ inheritance from C's const semantics. It surprises people who
expect every namespace-scope definition to be external. Check the linkage before you rely on
address identity.
6. Anonymous Namespaces
namespace
{
int implementation_counter;
void helper();
}
Names in an unnamed namespace have internal linkage. Each translation unit gets its own copy,
and the names cannot collide with the same names in another translation unit. It is the
modern replacement for static at namespace scope.
An anonymous namespace is useful for implementation details in a .cc file. It is
wrong for shared state in a header: every translation unit that includes the header gets a
different object, so the "global" is not global. If you want one object, use an
inline variable.
The anonymous namespace is also not a security boundary. It changes linkage, not visibility to the debugger, the binary or another translation unit that has its own copy. Do not put secrets in it.
7. Inline Namespaces
An inline namespace is a namespace whose members are also visible in the enclosing namespace:
namespace lib
{
inline namespace v2
{
struct widget
{
int id;
};
}
}
lib::widget w{1}; // OK, as if widget were directly in lib
lib::v2::widget x{2}; // also OK
This is the standard library's versioning tool. std::literals is an inline
namespace, which is why using namespace std::literals; makes the literals
visible without naming the version. A library can add a new inline namespace
v3 and keep the old one for ABI compatibility.
Inline namespaces affect name lookup and mangling. They are not a runtime mechanism and they do not create a new object. Use them when you need transparent versioning, not as a generic grouping tool.
8. extern: Declaration vs Definition
extern int global; // declaration: no storage here
int global = 42; // definition: storage here
extern "C" int c_api(); // C language linkage
extern without an initializer is a declaration. It tells the compiler the name
and type; the linker finds the definition elsewhere. A definition allocates storage (or
provides a function body). The distinction matters in headers: put declarations in headers
and definitions in one .cc file, or use inline for variables.
extern "C" is language linkage, not storage. It disables C++ name mangling so a
C program or a C ABI can find the symbol. It does not change the type system, and it does not
make a C++ class layout-compatible with C.
Key Takeaways
- Scope, storage duration and linkage are independent properties.
- A local
staticis one object per program, initialized once, thread-safe on first use. thread_localis one object per thread; it is private state, not synchronization.inlinevariables give one entity across translation units in a header.- Namespace-scope
consthas internal linkage by default; useinline constexprfor one identity. - Anonymous namespaces are for per-translation-unit implementation details, not shared globals.
- Inline namespaces are transparent versioning, not a runtime feature.
externdeclares; a definition provides storage.
The last page covers declarations whose meaning is easy to miss: conditional
explicit, empty-base optimization, alignment, offsetof and
volatile.