C++ / a working model

05 / 163   ·   C++11   ·   8 min

static: three contexts of storage duration, linkage, and class members

Keep this sentence

The meaning of static depends on where it appears: a local variable obtains static storage duration, a namespace-scope entity can obtain internal linkage, and a class static member does not belong to any one instance. Thread-safe initialization of a local static does not mean later reads and writes are automatically thread-safe.

In this lesson
  1. Local names, long-lived storage
  2. Internal linkage at namespace scope
  3. Shared class members and instance members
  4. Example
  5. Exercise

Local names, long-lived storage

A static local variable inside a function can still be accessed directly by name only within its scope, but its storage has static storage duration and does not automatically disappear when the function returns. The name stays local to the function; the object outlives each individual call. For a local static variable that needs dynamic initialization, initialization happens the first time execution reaches the declaration. Later calls reuse that same object instead of creating a new one with a fresh initializer.

From C++11 onward, when several threads first reach that declaration at the same time, initialization is protected by synchronization that the language itself guarantees. If initialization throws an exception, a later entry will retry. If, while initialization is still in progress, control re-enters the same declaration recursively, the behavior is undefined. Do not stretch the initialization guarantee into an automatic lock covering every later operation on the object. Once the object exists, ordinary reads and writes follow the usual data-race rules, so a local static counter is not magically safe for concurrent ++ just because its first initialization was synchronized.

Internal linkage at namespace scope

An ordinary non-thread-local variable at namespace scope already has static storage duration. In this position the important extra effect of static is to give the name internal linkage. Definitions that share a spelling can then exist separately in different translation units and are not the same entity. Functions can likewise be marked static so they are confined to the current translation unit and do not collide with a like-named function elsewhere.

An anonymous namespace is a common way to organize a group of file-internal entities without sprinkling static on every name. If you define a static mutable variable in a header, each translation unit that includes that header typically owns independent state. That arrangement is completely different from one counter shared by the whole program. You cannot infer a single address merely because two files appear to use the same variable name. Internal linkage means each translation unit may have its own object, with its own lifetime and its own value.

Shared class members and instance members

A static data member is not embedded in each object; every instance of the class shares that one member. A static member function has no this and cannot directly access an ordinary non-static member of some unspecified object. Writing Type::member makes it explicit that the access is not private per-instance state. The class provides the scope; an instance is not required merely to name the member.

A traditional non-inline static data member usually needs a definition outside the class. C++17 inline static data members can be defined inside the class, which avoids a separate source-file definition for many simple cases. The example only demonstrates that local static state is retained across calls, and it stores the two results in sequence so evaluation order inside a single output expression cannot confuse the core idea. The demonstration is about lifetime and reuse of one function-local object, not about class members or linkage.

Pitfalls

  • Incrementing a local static counter with ++ can still race under concurrent calls; initialization synchronization does not protect later increments.
  • Dynamic initialization dependencies across translation units easily become order problems; the static keyword itself does not fix global initialization order.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <iostream>

int next_id() {
    static int id = 0;
    return ++id;
}

int main() {
    const int first = next_id();
    const int second = next_id();
    assert(first == 1);
    assert(second == 2);
    std::cout << first << ' ' << second << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-static.cpp -o example && ./example

Expected result

1 2

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If a header contains static int count = 0; and both a.cpp and b.cpp include it, does incrementing count in a.cpp necessarily increase the value that b.cpp reads? How do you express shared state?

Show a reference answer

No. The two definitions have internal linkage and belong to their own translation units. You can write extern int count; in the header and int count = 0; in exactly one source file. In C++17 you can also put inline int count = 0; in the header. Either sharing style still needs separate synchronization for concurrent access.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog