Skip to content
C Codeloom
C++

C++ Undefined Behavior Pitfalls

A guided tour of the most common undefined behavior traps in C++ and the habits, tools, and language features that help you avoid them in production code.

·4 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • What undefined behavior means and why it matters
  • Common UB sources: lifetimes, aliasing, overflow
  • How modern compilers exploit UB during optimization
  • Sanitizers and tools that catch UB early
  • Defensive coding patterns that avoid UB by construction

Prerequisites

  • Comfort with C++ syntax, pointers, and basic memory model

What and Why

Undefined behavior (UB) is what the C++ standard says about a program that breaks the rules: anything is allowed to happen. The compiler can assume UB does not occur, so it optimizes around the assumption. The result is that a program with UB might work in debug, crash in release, or behave differently after an unrelated change.

Avoiding UB is not paranoia. It is the only way to write portable C++ code whose behavior survives the next compiler upgrade.

Mental Model

Think of UB as a contract violation. The standard gives you a long list of obligations. If you keep them, the compiler must produce code that behaves as written. If you break them, the compiler is free to do anything, including silently miscompile.

pointer/reference lifetime issues
 - dangling pointer, dangling reference
 - use after free, use after move (logical)

memory access
 - out-of-bounds array indexing
 - misaligned load/store, type punning via cast

integer/arithmetic
 - signed overflow, shift by >= width
 - division by zero

ordering / aliasing
 - data race on non-atomic
 - strict aliasing violation
 - reading uninitialized values
Common UB sources

Hands-on Example

Several short snippets, each a tiny landmine.

#include <iostream>
#include <vector>

int& dangling() {
    int x = 42;
    return x;            // UB: returning ref to local
}

int read_uninit() {
    int x;
    return x + 1;        // UB: x is uninitialized
}

void oob() {
    int a[3] = {1,2,3};
    std::cout << a[3];   // UB: index out of bounds
}

int signed_overflow(int n) {
    return n + 1;        // UB if n == INT_MAX
}

void alias_violation() {
    float f = 3.14f;
    int* p = reinterpret_cast<int*>(&f);
    int n = *p;          // UB: strict aliasing violation
}

Each of these compiles. Many run “correctly” in some build configurations. In an optimized build with a different compiler or flag set, they may produce wrong results or be silently deleted.

Common Pitfalls

  • Dangling references after move: a moved-from object is still alive but in an unspecified state. Reading it logically is fine; relying on its previous value is not.
  • Iterator invalidation: keeping an iterator into a std::vector across a push_back is UB if a reallocation happens. Each container has rules; learn them.
  • Type punning with casts: reinterpret_cast between unrelated types violates strict aliasing. Use std::memcpy or std::bit_cast (C++20) for safe punning.
  • std::vector<bool> and references: it does not store actual bools, so auto& b = vec[0] does not behave like a normal reference.
  • Signed overflow optimizations: compilers assume n + 1 > n for signed types. Loops written with signed counters can be turned into infinite loops if you overflow.

Practical Tips

Compile with warnings cranked up: -Wall -Wextra -Wpedantic -Wshadow -Wconversion. Many UB patterns are flagged before they ship.

Use sanitizers during development and CI:

  • AddressSanitizer (-fsanitize=address) catches out-of-bounds and use-after-free.
  • UndefinedBehaviorSanitizer (-fsanitize=undefined) flags overflow, misaligned access, and more.
  • ThreadSanitizer (-fsanitize=thread) finds data races.

These add runtime overhead but turn silent bugs into clear failures.

Lean on language features that remove UB by construction:

  • std::array and at() for bounds-checked access.
  • Smart pointers for ownership, eliminating manual delete mistakes.
  • std::optional and std::variant to express “no value” without raw pointers.
  • std::bit_cast for safe punning.
  • Initialize variables at declaration, ideally with = {} or a meaningful value.

For multithreading, every shared mutable state needs synchronization. Either lock it, make it atomic, or make it immutable. There is no in-between.

Run static analyzers (clang-tidy, cppcheck) as part of your build. They flag many UB patterns that warnings miss.

Wrap-up

Undefined behavior is the steepest cliff in C++. The good news is that modern tooling and modern language features cover most of the surface area. Initialize your variables, manage lifetimes with RAII, use bounds-checked operations during development, and let sanitizers loose on your test suite. Every time you eliminate a class of UB, you also unlock more aggressive compiler optimizations safely. The result is code that is both faster and harder to break.