Skip to content
VLSI Mentor

C++ Part I · Module 7 · Scope, Storage, and Namespaces

Static Objects and Their Initialization

An object with static storage duration lives for the whole program, which raises a question the other durations do not have to answer: an object that exists before main starts has to be initialized at some point, and something has to decide when. C++ gives two very different answers depending on where the object was declared, and the contrast between them is the whole of this chapter. For a static variable inside a function the answer is precise and genuinely useful — it is initialized the first time control passes through its declaration, exactly once, and not at all if that line is never reached, which means a function nobody calls costs nothing. For an object at namespace scope the standard is precise about one thing and deliberately silent about another, and the silence is the part with a reputation. Within a single source file the order is the order you wrote. Across source files there is no guaranteed order at all, and that is not a hypothetical: the program at the end of this chapter was compiled once and linked twice with its object files in two different orders, and it initialized its two objects in a different sequence each time.

Intermediate17 min readC++Part IFundamentalsScope

Part I · Module 7 · Chapter 7.4 · Static Objects and Their Initialization

1. One Duration, Two Initialization Stories

Chapter 7.3 §5 established that two different declarations produce static storage duration:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int at_namespace_scope = 1;                      // static duration

    void f() { static int inside_a_function = 2; }   // also static duration

Same duration, and §3 measured them four bytes apart in the same region. What the durations table did not say is when each one's initializer runs, and that is where they diverge completely:

InitializedOrder guaranteed?
static localon first pass through its declarationyes — §4
namespace-scope objectbefore main, during start-upwithin one file, yes; across files, no

The rest of the chapter is those two rows.

A note on what "initialization" means here. Every example uses an initializer expression — a value, or a call that produces one. Objects with constructors are Chapter 12.2's subject and they make this chapter's rules matter far more, because a constructor can have effects. The rules themselves are the same ones.

2. Before Anything Else: Zero

One guarantee applies to every object with static storage duration, regardless of where it was declared.

Before any other initialization happens, the storage is zeroed. An int with static duration starts as 0, a pointer as null, and so on, before any initializer you wrote has run.

This is the exception Chapter 3.8 §6 flagged. That chapter established that reading an uninitialized object is undefined behaviour and that an uninitialized local holds an indeterminate value — and noted that objects living for the whole program are different. This is the difference: they are zeroed first.

So:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int total;              // at namespace scope: guaranteed 0
    void f() { int total; } // automatic: indeterminate, reading it is UB

Two identical lines, two completely different situations, decided by storage duration. It is a good reason to write the initializer anyway — a reader should not have to work out which case they are looking at.

3. A Static Local Is Initialized Once

Azvya Education Pvt. Ltd.VLSI Mentor
onceinit.cpp — complete program
#include <iostream>

static int expensive_setup()
{
    std::cout << "  [expensive_setup() ran]\n";
    return 42;
}

static int cached()
{
    static int value = expensive_setup();   // initializer runs at most once
    return value;
}

int main()
{
    std::cout << "before any call\n";
    for (int call = 1; call <= 3; ++call) {
        const int v = cached();              // evaluate first...
        std::cout << "  call " << call << " -> " << v << '\n';   // ...then print
    }
    return 0;
}

Measured output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before any call
  [expensive_setup() ran]
  call 1 -> 42
  call 2 -> 42
  call 3 -> 42

Three readings.

expensive_setup() ran exactly once, not three times. The static int value = … line is executed on every call, and the initialization happens only the first time.

It ran during the first call, not before main. The line before any call printed first. Initialization is not hoisted to start-up; it happens where the declaration is, when control first reaches it.

The value persisted. 42 on all three calls — Chapter 7.2 §3 measured the same persistence for a counter, at one unchanging address.

A function-local static is initialized the first time control passes through its declaration, and not again.

The loop in main evaluates cached() into a const local before printing, which is deliberate: writing std::cout << cached() directly puts the setup message in the middle of the call 1 -> line, because the argument is evaluated before the stream sees it. Chapter 4.9 §5's ordering material is the reason, and separating the two statements is the fix.

4. Two More Guarantees

It is not initialized at all if control never reaches it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int cached() { static int v = setup(); return v; }
    int main() { std::cout << "never calling cached()\n"; return 0; }

Measured output — one line, and nothing from setup:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
never calling cached()

That is the property that makes a static local a reasonable lazy cache. A function nobody calls pays nothing, and an expensive setup is deferred until it is actually needed.

The initialization is thread-safe, since C++11. If two threads reach the declaration at once, exactly one performs the initialization and the other waits for it to finish. This is a real guarantee and worth knowing, because the idiom depends on it.

It is also one this curriculum cannot demonstrate: Part I creates no threads, and Module 26 is where concurrency is the subject. So treat it as stated rather than shown — and note that it applies specifically to function-local statics, not to the namespace-scope objects in §6.

5. Where a Static Local Is the Right Answer

Three cases, all of which you have now met:

A counter across calls. Chapter 7.2 §3's calls, and Chapter 6.6 §4 used one to show how often a default argument was evaluated.

A value computed once and reused. §3's cached(). The computation happens on first use and never again.

Something that must outlive every call but belongs to one function. The object's lifetime is the program's; its name is visible in exactly one place. That combination — long life, narrow visibility — is the module's two axes being used deliberately rather than suffered.

And one case where it is the wrong answer: shared mutable state. A static local that several calls write to is a global variable with a smaller name, and it brings every problem a global has — including, once threads exist, data races on the value that the initialization guarantee says nothing about. The guarantee in §4 covers initializing the object, not using it afterwards.

6. Namespace Scope: Within One File, in Order

For objects at namespace scope, initialization happens during start-up, before main runs.

Within a single translation unit the order is the order of declaration. If a is declared above b in one file, a is initialized first. That is guaranteed and you can rely on it.

The problem is that programs have more than one file.

7. Across Files: No Order At All

Here is the measurement. Four files: two translation units each defining one namespace-scope object whose initializer announces itself, a helper to do the announcing, and a main.

Azvya Education Pvt. Ltd.VLSI Mentor
report.h
#ifndef REPORT_H
#define REPORT_H
int announce(const char* who);
#endif
Azvya Education Pvt. Ltd.VLSI Mentor
report.cpp
#include "report.h"
#include <iostream>
int announce(const char* who)
{
    std::cout << "  initialising " << who << '\n';
    return 1;
}
Azvya Education Pvt. Ltd.VLSI Mentor
unit_a.cpp
#include "report.h"
int a_object = announce("a_object (unit_a.cpp)");
Azvya Education Pvt. Ltd.VLSI Mentor
unit_b.cpp
#include "report.h"
int b_object = announce("b_object (unit_b.cpp)");
Azvya Education Pvt. Ltd.VLSI Mentor
main.cpp
#include <iostream>
extern int a_object;
extern int b_object;
int main()
{
    std::cout << "main() starts; a_object=" << a_object << " b_object=" << b_object << '\n';
    return 0;
}

Compile all four once. Then link them twice, changing only the order the object files are given:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ main.o unit_a.o unit_b.o report.o -o prog_ab && ./prog_ab
  initialising a_object (unit_a.cpp)
  initialising b_object (unit_b.cpp)
main() starts; a_object=1 b_object=1

$ c++ main.o unit_b.o unit_a.o report.o -o prog_ba && ./prog_ba
  initialising b_object (unit_b.cpp)
  initialising a_object (unit_a.cpp)
main() starts; a_object=1 b_object=1

Same source. Same compiler. Same object files. Different order.

Nothing was recompiled between those two runs — only the order of the arguments to the linker changed, and the initialization order followed it.

This is usually described rather than demonstrated, and describing it understates the problem. It is not that the order is unknown; it is that the order is determined by something no part of your source code mentions, and which a build-system change can alter without anybody touching a line of C++.

What the standard says, precisely: the relative order of initialization of namespace-scope objects in different translation units is unspecified. Chapter 1.3 §5's grades apply — unspecified means the implementation must pick one of several allowed behaviours and need not tell you or be consistent. Both orders above are conforming.

8. Why It Matters, and the Remedy

In §7's program nothing goes wrong, because neither initializer reads the other object. Make one of them do so and the program's correctness depends on the link order:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // unit_a.cpp
    int a_object = announce("a");

    // unit_b.cpp
    extern int a_object;
    int b_object = a_object + 1;      // reads a_object during start-up

Measured, with only the link order changed between the two runs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ main.o unit_a.o unit_b.o report.o -o p_ab && ./p_ab
  initialising a_object
main(): a_object=1 b_object=2

$ c++ main.o unit_b.o unit_a.o report.o -o p_ba && ./p_ba
  initialising a_object
main(): a_object=1 b_object=1

b_object is 2 in one build and 1 in the other.

When a_object is initialized first, b_object becomes 2. When it is not, b_object reads a_object before its initializer has run — and §2's guarantee is what saves this from being undefined behaviour: the storage was zeroed, so the read gives 0 and b_object becomes 1.

Notice what the output does not show. Both runs print initialising a_object exactly once, and by the time main runs, a_object is 1 in both. The only visible difference is b_object's value. There is no garbage, no crash, and nothing in the program's output that says an ordering problem occurred — just a plausible-looking wrong number in one build configuration.

The remedy falls out of §3. Replace the object with a function containing a static local:

Azvya Education Pvt. Ltd.VLSI Mentor
the standard fix
    // instead of:  int a_object = announce("a");

    int& a_object()
    {
        static int value = announce("a");     // initialized on first use
        return value;
    }

Now initialization happens the first time somebody calls a_object(), which is necessarily after whatever needed it started running — so the order problem cannot arise. Any caller, in any translation unit, in any link order, gets an initialized object.

Measured, with the same two link orders that produced 2 and 1 above:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ fmain.o cfg.o user_b.o -o f1 && ./f1
main() starts
  initialising a_object (on first use)
  call 1: b_object() = 2
  call 2: b_object() = 2

$ c++ fmain.o user_b.o cfg.o -o f2 && ./f2
main() starts
  initialising a_object (on first use)
  call 1: b_object() = 2
  call 2: b_object() = 2

Identical, both ways. And the initialization line appears after main() starts and only once — §3's and §4's guarantees doing exactly what they promise.

This is usually called initialization on first use, and §4's two extra guarantees are what make it work: it happens once, and it is thread-safe. Note that it returns a reference, which Chapter 6.3 §6 warned against — and the warning does not apply here, because the object has static duration and outlives every call. That is the exception Chapter 6.3 said would arrive later, arriving.

9. What to Actually Do

Prefer a function-local static to a namespace-scope object whenever the object needs initialization that does anything.

Use initialization on first use for anything a caller in another translation unit might need during start-up.

Write the initializer even when zeroing would do. A reader should not have to deduce the duration to know the value.

Do not let one namespace-scope object's initializer read another one from a different file. Measured: the order follows the link command.

Treat a static local as one function's private long-lived state, not as shared mutable state.

Remember the guarantee's scope. Initialization is once and thread-safe; using the object afterwards is not covered.

Expect the destruction problem to be separate — Chapter 12.8.

10. Mistakes Worth Making Once

Expecting a static local's initializer to run at start-up. Measured: before any call printed first; the setup ran during call 1.

Expecting it to run on every call. Measured: once, across three calls.

Expecting it to run even when the function is never called. Measured: it did not run at all.

Relying on initialization order across translation units. Measured: changing only the link order changed the order.

Expecting that bug to crash. §2's zeroing means it produces a plausible wrong value instead.

Forgetting that namespace-scope statics are zeroed first. It is the exception to Chapter 3.8's rule, and the reason the bug is quiet.

Returning a reference from the first-use idiom and thinking it is Chapter 6.3 §6's defect. It is not — the object has static duration.

Thinking the thread-safety guarantee covers using the object. It covers initializing it.

11. Interview Relevance

"When is a function-local static initialized?" On the first pass through its declaration, once, and not at all if never reached — plus thread-safe since C++11. All four parts matter; the last two are what separate a memorised answer from a used one.

"What is the static initialization order problem?" The relative order across translation units is unspecified, so one namespace-scope object's initializer cannot safely depend on another's in a different file. Being able to say how you would demonstrate it — compile once, link twice in different orders — is stronger than describing it.

"How do you fix it?" Initialization on first use: a function returning a reference to a function-local static. Say why it works (initialization is deferred to a point that is necessarily after the caller started) and name a limit (destruction order is still Chapter 12.8's problem).

12. What You Should Be Able to Say Now

  1. Static storage duration comes from two different declarations with two different initialization stories.
  2. Every static-duration object is zeroed before any other initialization — the exception to Chapter 3.8's rule.
  3. That is why int total; at namespace scope is 0 and the same line in a function is indeterminate.
  4. A function-local static is initialized on the first pass through its declaration, once.
  5. Measured: before any call printed first, the setup ran during call 1, and the value was 42 on all three calls.
  6. Measured: if the function is never called, the initializer never runs.
  7. The initialization is thread-safe since C++11 — stated, not demonstrated, because Part I creates no threads.
  8. The guarantee covers initializing the object, not using it afterwards.
  9. Within one translation unit, namespace-scope objects initialize in declaration order.
  10. Across translation units the order is unspecified. Measured: the same object files linked in two orders initialized in two orders.
  11. The order followed something no line of the source mentions — the link command.
  12. A dependent initializer does not crash; §2's zeroing makes it read 0 and produce a plausible wrong value.
  13. The remedy is initialization on first use — a function returning a reference to a function-local static.
  14. Returning that reference is not Chapter 6.3 §6's defect, because the object has static duration; and destruction order remains Chapter 12.8's problem.

13. Exercises

Exercise 1 — predict the output. For §3's program with the loop changed to five calls, give every line in order.

Exercise 2 — count the runs. static int v = setup(); inside a function called 0, 1 and 7 times. How many times does setup() run in each case?

Exercise 3 — which value? Give the initial value and say whether reading it is defined: (a) int total; at namespace scope (b) int total; inside a function (c) static int total; inside a function (d) a parameter with no argument supplied and no default.

Exercise 4 — reproduce the fiasco. Describe the exact commands you would run to show that initialization order across two translation units is not fixed, without recompiling anything between the two demonstrations.

Exercise 5 — why no crash? int b_object = a_object + 1; in another file reads a_object before its initializer runs. Say what value it reads, which guarantee makes that defined, and why that is worse than a crash.

Exercise 6 — apply the remedy. Rewrite int config = load_config(); at namespace scope using initialization on first use. Give the function's signature, say when load_config() now runs, and say why returning a reference is safe here.

Exercise 7 — name the limit. For each, say whether initialization on first use solves it: (a) one global's initializer reading another's (b) two threads initializing the same static local at once (c) two threads incrementing it afterwards (d) a static object's destructor using another static object (e) start-up cost you want to avoid paying.

14. Summary

  • Static duration has two declarations and two initialization stories.
  • Every static-duration object is zeroed first — Chapter 3.8's exception.
  • A function-local static is initialized on first pass, once. Measured: setup ran during call 1, value 42 on all three.
  • Never called means never initialized. Measured.
  • Thread-safe since C++11 — stated, not shown; and it covers initialization only.
  • Within a file, declaration order. Across files, unspecified.
  • Measured: the same object files linked in two orders gave two initialization orders.
  • A dependent initializer reads 0 and produces a plausible wrong value rather than crashing.
  • The remedy is initialization on first use, and returning that reference is safe because the object has static duration.
  • Destruction order is a separate problem — Chapter 12.8.

Three chapters have now used static and two have had to apologise for it. Chapter 7.3 §5 named the wart directly: the keyword means one thing on a local inside a function — the storage duration this chapter has been about — and something entirely different at namespace scope, where it does not change the duration at all because the object already had it. What it changes there is the module's third question, the one Chapter 7.1 §1 listed and deferred: whether another translation unit can see the name. The next chapter answers it, and the answer is visible in the object file — the same place Chapter 6.8 found weak external, with a difference you can read off in one character.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.