C++ Part I · Module 3 · Types, Variables, and Initialization
Uninitialized Objects and Indeterminate Values
Chapter 3.7 pointed at two declarations that differ by two characters and said one of them leaves the object with no value at all. This chapter is what that means, and it is the one in this module where precision matters most — because the usual explanation is close enough to sound right and wrong in the way that costs people days. You have probably been told an uninitialized variable contains garbage. That is a serviceable first picture and it describes a likely implementation rather than the rule, and the gap between those two things is exactly where the hard bugs live. The rule is stricter and stranger: the object holds an indeterminate value, and reading it is undefined behaviour — which means the compiler is entitled to assume you never do, and to optimise on that assumption. You will also learn which declarations are quietly zero-initialized, because some are, and knowing which is the difference between a habit and a guess.
Foundation16 min readC++Part IFundamentalsUndefined Behaviour
Part I · Module 3 · Chapter 3.8 · Uninitialized Objects and Indeterminate Values
1. The Two Declarations
int error_count{}; // zero
int error_count; // no valueThe first is value initialization from Chapter 3.7 §7. It holds 0.
The second is default initialization. For a local object of a built-in type, that means nothing happens — no value is written. Storage is set aside and left as it is.
This chapter is about what that second object holds, and what happens when you read it.
2. "Garbage" — Useful Once, Then Misleading
The common explanation is that the object contains garbage: whatever bits happened to be in that memory from whatever used it last.
As a first picture that is fine, and it gets one important thing right — the program did not put a value there, so you have no reason to expect any particular one.
Now the correction, because this picture leads people somewhere wrong.
"Whatever bits happened to be there" describes a plausible implementation. It is a story about memory. The C++ rule is not a story about memory at all, and the difference matters:
The object holds an indeterminate value. Reading it is undefined behaviour.
Those are two technical terms, and §3 and §4 take them one at a time. The reason to care is practical rather than pedantic. If the rule were "you get whatever bits were there", then the value would be unpredictable but the program would still have a meaning. The actual rule says the program has no defined meaning at all — and a compiler is allowed to act on that, in ways that reach code nowhere near the variable.
3. Indeterminate Value
The first term:
An object that has been default-initialized but not given a value holds an indeterminate value.
"Indeterminate" is not "random" and it is not "zero". It is the standard declining to tell you anything. There is no value you can predict, no distribution it is drawn from, and no guarantee it even stays the same if you read it twice.
That last point is the one that breaks intuition. Under the "garbage" picture, the bits are sitting there, so reading twice gives the same answer. Under the actual rule there is no such promise — the compiler may have the object in a register one moment and not the next, and nothing requires consistency.
4. Why Reading It Is Undefined Behaviour
The second term, and Chapter 1.3 §5 already made it precise: undefined behaviour means the standard places no requirements on the program. Not "the value is unpredictable" — the whole program has no defined meaning.
For the built-in types in this module, reading an object that holds an indeterminate value is undefined behaviour.
The consequence that catches people is the one Chapter 3.3 §5 described for signed overflow, and it works the same way here. The compiler may assume undefined behaviour never happens. If your code reads an uninitialized object, the compiler has already reasoned using an assumption that is false, and the effects can show up somewhere else entirely — a branch simplified away, a check removed, a value the optimiser decided it already knew.
That is why "it printed 32767 on my machine" is not a useful observation. It is one run of one build. Change the optimisation level, add a line above, upgrade the compiler, and there is nothing to appeal to.
5. Which Declarations Are Zero-Initialized
Not every uninitialized-looking declaration leaves an indeterminate value. Three cases are quietly fine, and knowing which turns a superstition into a rule.
#include <iostream>
int namespace_scope_counter; // zero-initialised before main runs
int main()
{
static int static_local_counter; // zero-initialised
int value_initialised{}; // zero-initialised by the empty braces
std::cout << "namespace scope = " << namespace_scope_counter << '\n';
std::cout << "static local = " << static_local_counter << '\n';
std::cout << "value-init {} = " << value_initialised << '\n';
return 0;
}Expected output:
namespace scope = 0
static local = 0
value-init {} = 0Three different reasons, and the first two share one:
| Declaration | Zero-initialized? | Why |
|---|---|---|
int x; inside a function | No | Default initialization; indeterminate value |
int x; at namespace scope | Yes | Objects with static storage duration are zero-initialized before the program starts |
static int x; inside a function | Yes | Same reason — static gives it static storage duration |
int x{}; anywhere | Yes | Value initialization, which you asked for |
The dividing line is storage duration, which is Module 7.3's subject. The short version: an object that exists for the whole program is zero-initialized before main runs, because there is a well-defined moment to do it. A local object is created each time control reaches its declaration, and zeroing it every time would be a cost you did not ask for.
6. How You Actually Catch It
Since you cannot test for it reliably, detection matters more here than in most chapters.
The compiler warning is your main defence. Chapter 2.3 §5 already met this exact program:
#include <iostream>
int main()
{
int cycle_count = 0;
int error_count; // default-initialized: indeterminate
std::cout << "cycles = " << cycle_count << '\n';
std::cout << "errors = " << error_count << '\n'; // undefined behaviour
return 0;
}$ c++ -std=c++17 -Wall -Wextra -Wpedantic uninit.cpp -o uninit
uninit.cpp:9:33: warning: variable 'error_count' is uninitialized when used here
[-Wuninitialized]The file, the line, the column, the variable by name. That warning is the evidence this chapter relies on — not a printed value.
Now the honest part about tools, because it is easy to assume a sanitizer will catch anything.
The UndefinedBehaviorSanitizer does not catch this. Running the program above under -fsanitize=undefined produced no runtime error at all. UBSan detects a specific list of undefined operations — signed overflow among them, as Chapter 3.3 §5 showed — and reading uninitialized memory is not on it.
The tool for that is a different one, MemorySanitizer, and on the machine used for this chapter it is not available:
$ c++ -std=c++17 -fsanitize=memory uninit.cpp -o uninit
clang++: error: unsupported option '-fsanitize=memory'
for target 'arm64-apple-darwin25.5.0'So on this platform, for this bug, there is no runtime safety net. The compiler warning is what you have.
That changes the priority. For signed overflow you can write the code and let a sanitizer find the mistake. For uninitialized reads, the discipline has to be in how you write the declaration, because the tool that would catch it may not exist where you are working. Module 25 covers the sanitizers properly, including which platforms support which.
7. A Note on Which C++ This Is
Chapter 1.3 fixed this curriculum's baseline at C++17, and here is one place where the baseline genuinely matters.
Under C++17, reading an object holding an indeterminate value is undefined behaviour, with narrow exceptions involving unsigned char that do not affect anything in this module.
Later standards have revisited this. C++26 introduces a category that changes the treatment of some uninitialized reads, so that they are no longer the same unbounded undefined behaviour. That is a real change and it is not the rule for the C++ this curriculum teaches, nor for the toolchains Chapter 1.3 §3 described as typical in this field.
Two reasons this is worth a paragraph rather than silence:
- If you read about this elsewhere, you may meet newer terminology that does not match what is written here. Now you know why.
- The practical advice is the same under every version: initialize the object. A rule change that makes a bug slightly less catastrophic is not a reason to write the bug.
8. Mistakes Worth Making Once
Believing "it holds garbage" is the rule. That describes an implementation. The rule is an indeterminate value, and reading it is undefined behaviour — which is what permits the optimiser to act on an assumption that is now false.
Testing it and concluding it is fine. One run of one build proves nothing. Chapter 1.3 §5 showed the same source giving opposite answers at two optimisation levels.
Assuming locals are zeroed because statics are. Namespace-scope and static objects are zero-initialized; a plain local is not. The dividing line is storage duration.
Assuming a sanitizer will catch it. Measured: UBSan did not, and MemorySanitizer is unavailable on this platform. The compiler warning was the only detection.
Trusting -Wuninitialized to find every case. It found the obvious one. Across function boundaries and conditional writes, it frequently cannot prove anything and stays quiet.
Declaring now and assigning later out of habit. Every line between the two is a line where a read is undefined. Initialize on the declaring line.
Writing static int count; and relying on the guarantee. It is a real guarantee. Write {} anyway, so the next reader — and the next refactor — cannot get it wrong.
9. What You Should Be Able to Say Now
int x;inside a function is default initialization: for a built-in type, nothing is written.- The object then holds an indeterminate value, and reading it is undefined behaviour.
- "Garbage" describes a plausible implementation, not the language rule. The difference is what lets the compiler optimise on a false assumption.
- Indeterminate is not random and not zero, and there is no guarantee two reads agree.
- Namespace-scope objects and function-local
staticobjects are zero-initialized, because they have static storage duration. Plain locals are not. int x{};is zero-initialized anywhere, because you asked.- Write
{}even where the guarantee exists, so a reader can tell you meant it and a refactor cannot break it. - Measured:
-Wall -Wextra -Wpedanticreported the uninitialized read by file, line, column and variable name. - Measured: UBSan did not catch it, and MemorySanitizer is unavailable on this target. On this platform there is no runtime safety net for this bug.
-Wuninitializedcannot catch every case — across functions and conditional paths it often cannot prove anything.- The sufficient rule costs nothing: give every object a value on the line that declares it.
- The C++17 rule is undefined behaviour; later standards revisit it. The advice is unchanged.
10. Exercises
Exercise 1 — zero or indeterminate? For each, say which and why: (a) int a; at namespace scope; (b) int b; inside main; (c) static int c; inside a function; (d) int d{}; inside a function.
Exercise 2 — correct the explanation. A colleague says: "An uninitialized int just has whatever was in that memory before, so it is unpredictable but harmless if you overwrite it before use." Identify the one part that is right and the two parts that are wrong.
Exercise 3 — why no printed value? This chapter never printed an uninitialized variable. Explain the reasoning, and say what a reader would wrongly conclude if it had.
Exercise 4 — the detection gap. You write code that reads an uninitialized value. Say what the compiler is likely to do, what UBSan will do, and what MemorySanitizer would do if available — then say which of those you can rely on when working on an unfamiliar platform.
Exercise 5 — where the warning fails. Sketch a situation in which a variable is genuinely read before being written and -Wuninitialized stays silent. Explain what makes it hard for the compiler.
Exercise 6 — the refactor. static int error_count; sits inside a function and is relied on being zero. Someone removes static while tidying. Describe exactly what changes, whether anything warns, and which two characters would have prevented it.
Exercise 7 — state the rule twice. Write the C++17 rule for reading an indeterminate value in one sentence, then write the practical advice in one sentence, and explain why the second does not change when the first does.
11. Summary
int x;inside a function writes nothing. The object holds an indeterminate value and reading it is undefined behaviour.- "Garbage" is an implementation story, not the rule — and the rule is what permits the optimiser to assume the read never happens.
- Indeterminate is not random, not zero, and not guaranteed stable between reads.
- Namespace-scope and
staticlocals are zero-initialized; plain locals are not. The line is storage duration, taught in Module 7.3. - Write
{}anyway, so intent is visible and a refactor cannot silently break it. - The compiler warning is the main defence, and it named the file, line, column and variable.
- Measured: UBSan does not catch this, and MemorySanitizer is unavailable here. No runtime safety net on this platform.
- The warning is necessary and not sufficient — it cannot prove much across functions or conditional paths.
- The sufficient rule is free: give every object a value on the line that declares it.
- C++17 says undefined behaviour; later standards revisit it, and the advice does not change.
The last two chapters of this module are about saying more with a declaration than its type alone conveys. The first is a promise: that an object's value will not change after it is created. That turns out to be enforced by the compiler rather than suggested to the reader, and it is the beginning of a habit that runs through the whole language — stating intent in a form the compiler can check. Chapter 3.9 is const.
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.
