Question

Uninitialized bool and Undefined Behavior in C++

cppllvmundefined-behaviorabillvm-codegen

Question

Does the C++ standard allow a program to crash when it reads an uninitialized bool?

Consider a function that serializes a Boolean value as either "true" or "false":

#include <cstring>

char destBuffer[16];

void Serialize(bool boolValue) {
    const char* whichString = boolValue ? "true" : "false";
    const std::size_t len = std::strlen(whichString);
    std::memcpy(destBuffer, whichString, len);
}

Suppose the bool comes from an uninitialized member:

struct FStruct {
    bool uninitializedBool;

    FStruct() {} // Does not initialize uninitializedBool
};

int main() {
    FStruct structInstance;
    Serialize(structInstance.uninitializedBool);
}

With a particular compiler and optimization level, the compiler may optimize the string length calculation as though it were:

const std::size_t len = 5 - boolValue;

This can lead to a crash if the underlying storage contains an unexpected byte value. Is the compiler allowed to assume that a bool has only the values false or true (effectively 0 or 1), and is reading an uninitialized bool undefined behavior?

Short Answer

Reading the uninitialized bool is the bug: it has undefined behavior in the C++ language versions relevant to this code. The call to Serialize already reads the member before the function body begins. For defined programs, a bool value is either false or true, so an optimizer may use that fact to simplify calculations and control flow. Initialize every Boolean object before reading it.

Concept

A bool is C++'s Boolean type. Its meaningful values are:

false
true

When an integer is converted to bool, C++ produces a normal Boolean value:

bool a = 0;   // false
bool b = 42;  // true

The value 42 is not stored as a third Boolean state. The conversion produces true.

However, an uninitialized local object or member is different. Its storage has not been assigned a valid program value. Reading it is not equivalent to receiving a random but valid true or false value.

In this example:

Serialize(structInstance.uninitializedBool);

C++ must evaluate structInstance.uninitializedBool in order to pass it by value. That read happens before Serialize starts executing. Since the member was never initialized, the program has undefined behavior at that point.

Undefined behavior means the C++ standard does not impose reliable requirements on the program after the invalid operation. It may appear to work, print a plausible result, crash only with optimization enabled, or behave differently after an unrelated source change.

For valid executions, the compiler can reason that boolValue is a valid Boolean value. Therefore, optimizing code based on the possible values false and true is permitted. It does not need to preserve behavior for a program that reached the function by reading an uninitialized bool.

The important distinction is:

  • A valid bool can be used in conditions and represents false or true.
  • An uninitialized bool must not be read.
  • Raw storage containing an arbitrary byte is not a safe way to create or inspect a bool object.

This is why optimization can reveal initialization bugs that seemed harmless in debug builds.

Mental Model

Think of a bool as a switch with exactly two documented positions: off (false) and on (true).

An initialized switch is known to be in one of those positions. A compiler can safely simplify logic based on that rule.

An uninitialized bool is not a switch in an unknown documented position. It is more like trying to use a switch before it has been installed. Looking at it is already outside the rules of the program.

The ternary expression:

boolValue ? "true" : "false"

does choose between two strings for a valid Boolean value. It does not repair the earlier mistake of reading an uninitialized object.

Take Quiz

Syntax and Examples

Initialize Boolean variables and members at their declaration whenever possible.

bool isReady = false;
bool hasPermission = true;

For class members, use a default member initializer:

struct FStruct {
    bool isEnabled = false;
};

int main() {
    FStruct instance;
    // instance.isEnabled is safely false
}

You can also initialize in a constructor member-initializer list:

struct FStruct {
    bool isEnabled;

    FStruct() : isEnabled(false) {}
};

A safe serialization function can accept a properly initialized Boolean value:

#include <cstring>

char destBuffer[16]{};

void Serialize(bool boolValue) {
    const char* text = boolValue ? "true" : "false";
    std::strcpy(destBuffer, text);
}

int main() {
    bool enabled = true;
    Serialize(enabled);
}

Here, enabled is initialized before it is read and passed to Serialize. std::strcpy is safe in this specific example because destBuffer has room for both "true" plus its null terminator and "false" plus its null terminator.

Step by Step Execution

Consider this safe program:

#include <cstring>

char destBuffer[16]{};

void Serialize(bool boolValue) {
    const char* text = boolValue ? "true" : "false";
    const std::size_t length = std::strlen(text);
    std::memcpy(destBuffer, text, length);
}

int main() {
    bool active = false;
    Serialize(active);
}

Execution proceeds as follows:

  1. active is initialized with false.
  2. Serialize(active) reads a valid Boolean value and passes it to boolValue.
  3. boolValue ? "true" : "false" selects "false".
  4. std::strlen("false") returns 5.
  5. std::memcpy copies the five letters f, a, l, s, and e into destBuffer.
  6. The buffer was zero-initialized, so its next byte remains \0 and it can be treated as a C string.

Now compare the unsafe call:

FStruct instance;
Serialize(instance.uninitializedBool); // Undefined behavior: read occurs here.

The failure is not caused by strlen or the ternary operator. The invalid read occurs while evaluating the argument to Serialize.

Real World Use Cases

Boolean initialization matters anywhere a flag controls program behavior:

  • Feature flags: bool useNewCheckout = false; prevents accidentally enabling an unfinished feature.
  • Authentication state: Initialize isAuthenticated to false until credentials are verified.
  • Network requests: Initialize hasTimedOut, isConnected, and shouldRetry before checking them.
  • Parsing: Set flags such as foundHeader or hasError to false before processing input.
  • Resource ownership: Initialize state flags before deciding whether a handle should be closed or released.
  • Serialization: Initialize fields before converting them to JSON, text, database values, or network messages.

An uninitialized flag can cause more than a crash. It can skip validation, choose the wrong configuration, incorrectly report success, or take an unsafe cleanup path.

Take Quiz

Real Codebase Usage

In production C++ code, developers avoid uninitialized state by making valid defaults part of a type's definition.

Default member initializers are usually the clearest approach:

struct RequestOptions {
    bool followRedirects = true;
    bool verifyTls = true;
    bool includeCookies = false;
};

This makes every normally constructed RequestOptions object usable immediately.

Constructors should initialize every member that has no suitable universal default:

class Connection {
public:
    explicit Connection(bool secure)
        : secure_(secure), connected_(false) {}

private:
    bool secure_;
    bool connected_;
};

Functions commonly use Boolean flags with guard clauses:

bool SaveDocument(const Document& document) {
    if (!document.isValid()) {
        return false;
    }

    // Save only after validation succeeds.
    return true;
}

For optional state, use a type that represents absence explicitly rather than a Boolean whose meaning is unclear:

#include <optional>

std::optional<bool> userPreference;

Here, std::nullopt means “no preference has been provided,” while false and true retain their normal meanings.

Common Mistakes

Assuming an uninitialized value is merely random

This is unsafe:

bool loggedIn;
if (loggedIn) { // Undefined behavior
    // ...
}

Do not assume it will act like a random true or false. Initialize it:

bool loggedIn = false;

Empty constructors do not initialize fundamental members

struct Settings {
    bool darkMode;
    Settings() {} // darkMode remains uninitialized
};

Fix it with a member initializer:

struct Settings {
    bool darkMode;
    Settings() : darkMode(false) {}
};

Or prefer a default member initializer:

struct Settings {
    bool darkMode = false;
};

Believing a condition sanitizes a Boolean

bool value; // uninitialized
const char* text = value ? "yes" : "no"; // Undefined behavior

A condition consumes the value; it cannot validate or initialize it.

Relying on debug-build behavior

A debug build may happen to fill memory with recognizable byte patterns, zero some memory, or arrange the stack differently. None of that makes an uninitialized read valid. Test optimized builds and use compiler warnings and runtime sanitizers.

Using memset as a general object initialization strategy

For a bool member, this is not a good general pattern:

Settings settings;
std::memset(&settings, 0, sizeof(settings));

Although zeroing simple storage can appear to work in limited cases, it is fragile and inappropriate for types with constructors, invariants, pointers with special requirements, or non-trivial members. Use normal C++ initialization instead.

Forgetting the null terminator during string copying

The original memcpy copies only the characters, not the terminating \0:

std::memcpy(destBuffer, text, std::strlen(text));

It is only usable as a C string afterward because the specific global buffer was zero-initialized and the copied text is shorter than the buffer. Prefer a clearer operation when appropriate:

std::strcpy(destBuffer, text);

Or, in modern C++, prefer std::string when a fixed C buffer is not required.

Comparisons

SituationIs it initialized?Safe to read?Result
bool value = false;YesYesfalse
bool value = 7;YesYesInteger conversion produces true
bool value; followed by a readNoNoUndefined behavior
struct S { bool value; }; S s{};Yes, for this aggregateYesvalue is zero-initialized to false
struct S { bool value; S() {} }; S s;NoNoThe user-provided constructor does not initialize value

bool differs from integer types in intent:

TypeIntended range of valuesTypical use
boolfalse or trueYes/no state and conditions
intMany integer valuesCounts, indexes, arithmetic
enum classNamed finite statesMore than two meaningful states

If you need states such as unknown, disabled, and enabled, a Boolean is often too limited. Use an enum:

enum class FeatureState {
    unknown,
    disabled,
    enabled
};
Take Quiz

Cheat Sheet

  • Initialize every bool before reading it:

    bool done = false;
    
  • Prefer member defaults:

    struct Job {
        bool cancelled = false;
    };
    
  • An empty constructor does not initialize built-in members:

    Job() {} // not enough if members have no default initializers
    
  • Initialize constructor members explicitly when needed:

    Job() : cancelled(false) {}
    
  • Integer-to-Boolean conversion is defined:

    bool a = 0;   // false
    bool b = -1;  // true
    
  • Reading an uninitialized bool is undefined behavior in the C++ versions applicable to this example.

  • The bad read may occur at argument passing:

    Serialize(object.flag); // `flag` is read before Serialize runs
    
  • Optimizers may assume defined code uses valid Boolean values and may transform code accordingly.

  • Enable warnings, such as -Wall -Wextra -Wuninitialized where supported, and use MemorySanitizer when available to find uninitialized reads.

FAQ

Can a C++ bool hold the value 2?

A properly created and read C++ Boolean value is used as either false or true. Assigning or converting an integer such as 2 to bool produces true. Do not create Boolean objects by writing arbitrary raw bytes into their storage.

Does if (uninitializedBool) safely convert the value to true or false?

No. The if condition must first read the bool, and reading the uninitialized object is already undefined behavior.

Why does the crash happen only with optimization enabled?

Optimizers transform code based on the rules for defined programs. An uninitialized read violates those rules, so a change in optimization level, compiler version, or surrounding code can expose different symptoms.

Is the ternary operator the problem?

No. The ternary operator is safe when its condition is a valid Boolean expression. The problem is the earlier read of an uninitialized member passed into the function.

Does {} always initialize a class member to false?

It depends on how the type is constructed. For a simple aggregate such as struct S { bool flag; };, S s{}; value-initializes flag to false. A user-provided default constructor can change initialization behavior, so default member initializers are the clearest and safest choice.

How should I initialize a Boolean member?

Prefer this:

struct S {
    bool flag = false;
};

Use a constructor initializer when the initial value depends on constructor arguments.

Can a compiler warning always find uninitialized Boolean reads?

No. Warnings are useful but not complete, especially across function calls and complex control flow. Initialize values by design and use sanitizers during testing.

Take Quiz

Mini Project

Description

Create a small configuration object for a command-line or service application. The object contains Boolean options with safe defaults and a function that converts one option to the text required by a configuration file. This demonstrates that an object's state should be valid immediately after construction.

Goal

Build and serialize a configuration object without ever reading an uninitialized Boolean member.

Requirements

  • Create a Config struct with Boolean members that have explicit default values.
  • Provide a function that converts a bool to either "true" or "false".
  • Serialize the verboseLogging setting into a destination character buffer.
  • Print the serialized setting.
  • Change the setting to true and serialize it again.
Take Quiz

Keep learning

Related questions

View more