Undefined Behavior

By the end of this lesson you'll be able to recognise the most common sources of undefined behavior (UB) in C++, explain why "it works on my machine" is never proof of correctness, understand how the optimizer exploits UB, and catch it automatically with sanitizers — replacing each dangerous pattern with a safe alternative.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

UB is like an unsigned legal contract with a blank clause . The C++ standard is the contract; most operations have spelled-out rules. But a handful — reading past an array, dereferencing a freed pointer — fall into a clause that simply says "results are undefined" . The compiler is then free to interpret that blank however makes your program fastest, including assuming you'd never trigger it. So when you do trigger it, there's no rule protecting you: the program might crash, might print nonsense, or might silently behave differently after the next recompile. UB isn't "a bug that crashes" — it's "a bug with no guaranteed behavior at all ", which is far worse, because you can't even rely on it failing.

UBSan = -fsanitize=undefined , ASan = -fsanitize=address , TSan = -fsanitize=thread , MSan = -fsanitize=memory . None of these slow your release build — you run them in debug and test builds.

1. Common UB — and the Safe Fix Beside It

The fastest way to learn UB is to see the dangerous line right next to its safe replacement. In the worked example below, every UB line is commented out (so the program still runs) and the safe version is live. Read each comment, run it, and notice that the fixes — .at() , a null check, initializing, a wider type — cost almost nothing.

Notice the pattern: the bounds-checked call v.at(5) throws instead of quietly corrupting memory, and a wider type holds a value that would overflow a 32-bit int . The unsafe versions might appear to work — that's exactly why UB is so treacherous.

2. How the Compiler Exploits UB

Here's the part that surprises everyone. The optimizer is allowed to assume your program never has UB . So if you dereference a pointer, it concludes the pointer can't be null — and may delete a null check you wrote afterwards, because (in its reasoning) that check could only matter in a case that already invoked UB. Your safety net silently disappears. The rule that saves you is simple: check before you act, never after .

Now you try. The program below contains UB. Fill in the two blanks marked ___ to make every access safe, using the hints in the comments.

3. Lifetime UB: Dangling References & Use-After-Free

A dangling reference points at memory whose owner has already been destroyed; use-after-free touches memory you've delete d. Both are UB and both are among the hardest bugs to find, because the freed memory often still holds the old value — until something else reuses it. The cure is ownership: return by value instead of returning references to locals, and let std::unique_ptr own heap memory so it can never outlive its owner.

4. Signed Overflow & Catching UB with Sanitizers

Signed integer overflow — like INT_MAX + 1 — is UB (unsigned overflow, by contrast, is defined and wraps around). You prevent it by checking before you add, widening the type, or using an unsigned counter. But you can't eyeball UB reliably, so the real workhorses are sanitizers : build with -fsanitize=undefined (UBSan) and -fsanitize=address (ASan) plus -Wall -Wextra , run your tests, and the tools print the exact file and line where UB occurs.

This last worked example isn't something to run for output — it's the exact compile command you'll use to catch UB in your own projects. Read the flags and the sample sanitizer reports.

Two more UB sources catch intermediate programmers. An invalid downcast happens when you static_cast a base pointer to a derived type the object isn't — the compiler trusts you, and using the result is UB. Use dynamic_cast (which returns nullptr on a bad cast) when you're not certain of the runtime type.

A data race is two threads accessing the same variable at the same time with at least one writing, and no synchronisation — also UB. Protect shared state with a std::mutex or make it std::atomic . The thread sanitizer ( -fsanitize=thread ) finds these.

No blanks this time — just a brief and an outline. Write a lookup that never invokes UB no matter what index it's given. Build it, run it, and check your output against the examples in the comments.

Practice quiz

What does undefined behavior (UB) mean in C++?

  • A guaranteed crash
  • A compiler warning
  • An operation the standard places no requirements on
  • A slow operation

Answer: An operation the standard places no requirements on. UB is an operation the standard leaves with no rules. The program may crash, print garbage, or appear to work — which is what makes it dangerous.

If a program with UB produces the right output today, what can you conclude?

  • Nothing — it may fail after a recompile or on another platform
  • The code is correct and UB-free
  • The UB has been fixed
  • The compiler removed the UB

Answer: Nothing — it may fail after a recompile or on another platform. UB can produce correct output by luck. 'It works' is never proof; only sanitizers and careful reasoning show code is UB-free.

Which of these is NOT undefined behavior?

  • Signed integer overflow (INT_MAX + 1)
  • Reading past the end of an array
  • Dereferencing a null pointer
  • Unsigned integer overflow (wraps modulo 2^N)

Answer: Unsigned integer overflow (wraps modulo 2^N). Unsigned overflow is fully defined — it wraps modulo 2^N. Only signed overflow is UB; the other listed operations are also UB.

Why can the optimizer delete a null check placed AFTER a pointer dereference?

  • Null checks are always redundant
  • It assumes UB never happens, so a prior deref 'proves' the pointer is non-null
  • Checks after dereferences are syntax errors
  • The compiler runs checks at link time

Answer: It assumes UB never happens, so a prior deref 'proves' the pointer is non-null. The optimizer assumes no UB. After *p it concludes p is non-null and may remove a later if (p == nullptr). Check before you dereference.

Which sanitizer catches out-of-bounds access and use-after-free?

  • ASan (-fsanitize=address)
  • UBSan (-fsanitize=undefined)
  • TSan (-fsanitize=thread)
  • -Wall only

Answer: ASan (-fsanitize=address). AddressSanitizer (ASan, -fsanitize=address) catches memory errors like out-of-bounds, use-after-free, and leaks.

Which sanitizer catches signed overflow, invalid shifts, and bad casts?

  • ASan (-fsanitize=address)
  • TSan (-fsanitize=thread)
  • UBSan (-fsanitize=undefined)
  • MSan (-fsanitize=memory)

Answer: UBSan (-fsanitize=undefined). UndefinedBehaviorSanitizer (UBSan, -fsanitize=undefined) catches language-level UB such as signed overflow, bad shifts, and bad casts.

What is the safe fix for returning a reference to a local variable?

  • Mark the local static
  • Return by value instead
  • Use a const reference
  • Add a virtual destructor

Answer: Return by value instead. Returning a reference to a local that dies at the closing brace is a dangling reference (UB). Return by value so the caller gets its own copy.

Why does v.at(5) on a size-3 vector behave more safely than v[5]?

  • at() is faster
  • at() returns 0 on bad index

operator[] does no bounds check, so v[5] is UB. at() checks the index and throws std::out_of_range instead of corrupting memory.

A data race (two threads, one unguarded shared variable, at least one writing) is what?

  • Always safe
  • Undefined behavior
  • Defined behavior
  • A compiler error

Answer: Undefined behavior. Unsynchronized concurrent access with at least one write is a data race, which is UB. Guard with a std::mutex or make it std::atomic.

How do you safely prevent signed overflow in a + b for ints?

  • Cast the result to unsigned
  • Wrap the add in try/catch
  • Check a > INT_MAX - b before adding (or widen the type)

Answer: Check a > INT_MAX - b before adding (or widen the type). Check before you add: if a > INT_MAX - b the addition would overflow. Alternatively use a wider type or an unsigned counter.