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

The last page covers declarations whose meaning is easy to miss: conditional explicit, empty-base optimization, alignment, offsetof and volatile.