20 topics
← Back to Quick Reference/
Topic 18
CCoommmmoonn BBeeggiinnnneerr MMiissttaakkeess
UB · Initialization · Memory · Type System · STL · Diagnostic Tools
C++17 · Advanced ReferenceC++ Mistake Categories
01Why C++ mistakes are dangerous
Many C++ mistakes produce Undefined Behavior (UB) — the compiler is free to do anything, including producing code that appears to work correctly in debug builds but silently corrupts data in release builds. The compiler actively exploits UB for optimization, which is why sanitizers are essential.
Mistake categories
- 1.
Undefined Behavior— signed overflow, null deref, OOB access, data races - 2.
Initialization— reading uninitialized locals, member init order, SIOF - 3.
Memory— leaks, double-delete, delete vs delete[], dangling refs - 4.
Type system— integer division, signed/unsigned comparison, narrowing - 5.
STL misuse— iterator invalidation, map[], off-by-one, endl spam
Defence strategy
- 1.Compile with
-Wall -Wextra -Werror— catch mistakes at compile time. - 2.Use
-fsanitize=address,undefinedin CI — catches runtime UB. - 3.Prefer modern C++ —
std::vectorover raw arrays,unique_ptrovernew, range-for over index loops. - 4.Run a static analyzer (
clang-tidy,cppcheck) on every PR.
Most C++ bugs caught in production could have been caught at compile time with stricter warnings, or at test time with sanitizers.
Undefined Behavior
02// Undefined Behavior — the compiler assumes UB never happens // and may optimize assuming it's unreachable // ── Signed integer overflow ─────────────────────────────────── int x = INT_MAX; x + 1; // ❌ UB — compiler may assume this never occurs // Fix: use unsigned for wrapping, or check before adding // ── Null / dangling pointer dereference ─────────────────────── int* p = nullptr; *p = 5; // ❌ UB — segfault on most platforms, silently wrong on others int* dangling; { int x = 5; dangling = &x; } *dangling; // ❌ UB — x is destroyed // ── Out-of-bounds array access ─────────────────────────────── int arr[5]; arr[5] = 0; // ❌ UB — one past the end // ── Use after move ──────────────────────────────────────────── std::string s = "hello"; std::string t = std::move(s); s.size(); // 🟡 valid (moved-from is a valid empty state for string) // But for user types, moved-from state may be unspecified // ── Detect: -fsanitize=address,undefined ───────────────────── // AddressSanitizer: out-of-bounds, use-after-free, stack overflow // UBSanitizer: signed overflow, null deref, misaligned access
| signed overflow | UB — the compiler assumes it never occurs and optimizes accordingly. Use unsigned for wrapping arithmetic. |
| null deref | UB — always null-check pointers before dereferencing, especially from find() or optional APIs. |
| out-of-bounds | UB — no exception, no crash guaranteed. Use .at() or enable AddressSanitizer. |
| use-after-move | Moved-from objects are in a valid but unspecified state. Reassign before reuse. |
Build with
-fsanitize=address,undefined during development. ASan + UBSan catches the vast majority of memory and UB bugs at ~2× runtime overhead — far cheaper than debugging in production.Initialization Mistakes
03// ── Uninitialized local variables ──────────────────────────── int x; // garbage value — reading x is UB std::cout << x; // ❌ UB — may print anything or crash // Fix: always initialize: int x = 0; or int x{}; // ── Static initialization order fiasco (across TUs) ────────── // file a.cpp int Registry::count = 0; // file b.cpp — may be initialized BEFORE a.cpp extern int Registry::count; int derived = Registry::count + 1; // ⚠ may be 0 // Fix: use function-local statics (initialized on first call) int& getCount() { static int n = 0; return n; } // ── Member initialization order ────────────────────────────── struct Bad { int b; int a; Bad() : a(5), b(a * 2) {} // ⚠ b initialized first! a is garbage here // Members init in DECLARATION ORDER, not initializer list order }; // Fix: reorder members or use a in the body after all inits
| uninitialized local | Reading an uninitialized local variable is UB. Always initialize: int x{} or int x = 0. |
| SIOF | Static Initialization Order Fiasco — globals in different .cpp files may init in any order. Use function-local statics. |
| member init order | Members initialize in declaration order, not the order listed in the initializer list. Reorder the declaration to fix. |
Memory Mistakes
04// ── Double delete ───────────────────────────────────────────── int* p = new int(5); delete p; delete p; // ❌ UB — corrupts the heap // Fix: null after delete, or better: use unique_ptr // ── Memory leak ─────────────────────────────────────────────── void leak() { int* p = new int[100]; if (error) return; // ❌ new[] never freed on this path delete[] p; } // Fix: use std::vector<int>(100) or unique_ptr<int[]> // ── delete vs delete[] ──────────────────────────────────────── int* arr = new int[10]; delete arr; // ❌ UB — must use delete[] delete[] arr; // ✅ // ── Stack buffer overflow ───────────────────────────────────── char buf[8]; std::strcpy(buf, "this string is too long"); // ❌ UB // Fix: use std::string — no fixed buffer // ── Returning local reference ───────────────────────────────── int& bad() { int x = 5; return x; } // ❌ x destroyed on return
| double delete | Corrupts the heap — undefined behavior. Null after delete, or use unique_ptr. |
| delete vs delete[] | new T[] must be paired with delete[], not delete. Wrong pairing is UB. |
| memory leak | new without delete. Use RAII: vector, unique_ptr, or any standard container. |
| dangling ref | Returning a reference to a local variable. The compiler may not warn — enable -Wreturn-local-addr. |
Type System Mistakes
05// ── Integer division truncates ──────────────────────────────── int a = 5, b = 2; double ratio = a / b; // ❌ 2.0 — integer division first! double ratio2 = (double)a / b; // ✅ 2.5 // ── Signed / unsigned comparison ───────────────────────────── int s = -1; unsigned u = 1; s < u; // ❌ false! -1 wraps to huge unsigned value // Fix: cast to the same type or use std::cmp_less (C++20) // ── Narrowing in assignments ────────────────────────────────── double d = 3.99; int i = d; // ⚠ i = 3 (truncated, no warning without -Wconversion) int j{d}; // ❌ compile error — brace init prevents narrowing // ── Implicit bool conversion surprises ──────────────────────── std::string s = "hello"; if (s) { } // ❌ compile error — string doesn't convert to bool if (!s.empty()) { } // ✅ // ── char signedness ─────────────────────────────────────────── char c = 200; // may be -56 if char is signed (platform-dependent) // Fix: use unsigned char or uint8_t for byte values
| integer division | 5/2 == 2. Cast one operand to double before dividing: (double)a / b. |
| signed/unsigned cmp | -1 < 1u is false. Enable -Wsign-compare. Use C++20 std::cmp_less for safe comparison. |
| narrowing | double → int silently truncates with = but is a compile error with {}. Prefer brace init. |
| char signedness | char signedness is platform-defined. Use unsigned char or uint8_t for byte manipulation. |
STL Mistakes
06// ── Iterator invalidation ──────────────────────────────────── std::vector<int> v = {1,2,3,4,5}; for (auto it = v.begin(); it != v.end(); ++it) { if (*it == 3) v.erase(it); // ❌ it is now invalid! } // Fix: use erase-remove idiom v.erase(std::remove(v.begin(), v.end(), 3), v.end()); // ── Off-by-one with indices ─────────────────────────────────── for (int i = 0; i <= v.size(); i++) { // ❌ i == v.size() is OOB v[i]; } // Fix: i < v.size() (or use range-for) // ── std::map operator[] creates entries ────────────────────── std::map<std::string, int> m; m["key"]; // ❌ inserts "key"→0 if not present! // Fix: use m.find("key") or m.count("key") // ── Copying when you should move ───────────────────────────── std::vector<std::string> words = {"hello","world"}; std::string s = words[0]; // copy std::string t = std::move(words[0]); // move — words[0] is now empty // ── endl performance ────────────────────────────────────────── for (int i = 0; i < 1000; i++) std::cout << i << std::endl; // ❌ flushes buffer 1000 times — slow // Fix: use "\n"
| iterator invalidation | Erasing from a vector inside a loop invalidates the iterator. Use erase-remove idiom instead. |
| map operator[] | m[key] inserts a default value if key doesn't exist. Use m.find() or m.contains() (C++20) to check. |
| endl vs \n | endl = newline + flush. Using it in a tight loop flushes the buffer every iteration — use \n. |
| off-by-one | i <= v.size() accesses v[v.size()] which is out-of-bounds UB. Always use i < v.size(). |
Diagnostic Tools
07# ── Compiler warnings — enable all ─────────────────────────── g++ -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror # ── Runtime sanitizers (catch UB and memory errors) ────────── g++ -fsanitize=address,undefined -g -O1 # AddressSanitizer: OOB, use-after-free, stack overflow, leaks # UBSanitizer: signed overflow, null deref, misaligned access # ── Valgrind (Linux) — memory leak and error detection ──────── valgrind --leak-check=full --error-exitcode=1 ./program # ── Static analyzers ────────────────────────────────────────── clang-tidy main.cpp -- -std=c++17 # catches many common mistakes cppcheck --enable=all main.cpp # lightweight, fast # ── Address sanitizer is the fastest first check ───────────── # It finds ~80% of memory bugs at 2× slowdown # Run it in CI on every build # ── clang-tidy checks to enable ────────────────────────────── # -checks=cppcoreguidelines-* (C++ Core Guidelines) # -checks=modernize-* (prefer modern C++ idioms) # -checks=bugprone-* (common bug patterns)
| -Wall -Wextra | Enable all standard and extra warnings. Add -Werror to make warnings into errors. |
| -fsanitize=address | AddressSanitizer — catches OOB, use-after-free, leaks. ~2× slowdown. Use in CI. |
| -fsanitize=undefined | UBSanitizer — catches signed overflow, null deref, misaligned access. Low overhead. |
| clang-tidy | Static analysis — catches patterns that compile cleanly but are likely bugs or non-idiomatic. |