interviewpath
  • 0day streak
  • 10total XP
  • Level 1
0/7

Unit 2 · Software engineering · Lesson 9 of 15

C++ fundamentals: lifetime, ownership and undefined behavior

C++ gives direct control over object lifetime, memory layout and concurrency, and with it whole classes of bugs that other languages rule out. Low-level, infrastructure and AI-infrastructure interviews probe whether you can reason about ownership, moves, iterator invalidation, virtual dispatch, undefined behavior and atomics precisely enough to write code that is correct rather than merely compiling.
About 6 hFocus mode

Step 1: Learn the ideas

  • RAII, scope and destructors during stack unwinding
  • Object lifetime, temporaries and dangling references
  • References versus pointers
  • Value categories, rvalue references and move semantics
  • Rule of zero and rule of five
  • Standard containers and iterator invalidation
  • unique_ptr, shared_ptr, weak_ptr and ownership cycles
  • Virtual dispatch, virtual destructors and slicing
  • Undefined behavior and sanitizers
  • std::atomic and memory_order basics

Why it matters

C++ compilers accept many programs whose behavior is undefined, and those bugs often surface only under optimization, load or a new toolchain. Low-level, infrastructure, database and AI-infrastructure interviews therefore test reasoning more than syntax: who owns this object, when does it die, what did that push_back do to the reference you were holding, and which thread is guaranteed to see which write.

Core ideas

RAII and lifetime. Tie every resource (memory, file descriptors, locks, GPU buffers) to an object whose destructor releases it. Automatic objects are destroyed in reverse order of construction, including during stack unwinding, so std::lock_guard, std::unique_ptr and containers make cleanup exception-safe without finally. The price is lifetime discipline: a reference, pointer or view must never outlive its object. Classic dangling cases are returning a reference to a local, a std::string_view into a temporary string, a lambda that captures locals by reference and runs later, and a reference into a vector held across an insertion.

References versus pointers. A reference is an alias that must be initialized and cannot be reseated; in a well-defined program it is never null. A pointer is an object that can be null, reassigned and incremented. Prefer references for required arguments, pointers or std::optional for optional ones, and smart pointers only to express ownership.

Move semantics. An rvalue reference (T&&) binds to objects about to expire, so a move constructor can take over their buffers instead of copying. std::move is only a cast to an rvalue reference; the transfer happens in whichever constructor or assignment is then selected. Moving from a const object silently copies, because the move constructor cannot bind to const T&&. Standard library types leave a moved-from object valid but unspecified: assign to it or destroy it, do not rely on its value. Declare move operations noexcept; otherwise std::vector copies copyable elements when it reallocates, to keep its strong exception guarantee. Prefer the rule of zero (let members such as std::vector and std::unique_ptr own resources); if you write any destructor, copy or move operation, consider all five.

Containers and invalidation. std::vector is contiguous and usually the fastest choice; a push_back beyond capacity reallocates and invalidates every iterator, pointer and reference into it. Rehashing an std::unordered_map invalidates iterators but not references to elements. std::map and std::list keep iterators valid except for erased elements. When erasing inside a loop, continue from the iterator that erase returns.

Smart pointers. unique_ptr expresses sole ownership, is move-only and in practice is the size of a raw pointer with the default deleter. shared_ptr adds a reference-counted control block with atomic count updates; make_shared typically allocates object and block together. Two objects holding shared_ptrs to each other never reach a count of zero and leak; make the back edge a weak_ptr and call lock() when you need the object.

Virtual dispatch. Mainstream implementations give each polymorphic object a hidden pointer to a per-class table of function pointers (a vtable); a virtual call loads the entry and calls through it, which blocks inlining unless the compiler can devirtualize. Deleting a derived object through a base pointer is undefined unless the base destructor is virtual. Passing a derived object by value as its base slices off the derived part, so pass polymorphic objects by reference or pointer.

Undefined behavior. Signed overflow, out-of-bounds access, use after free, data races on non-atomic objects and accessing an object through an incompatible type (a strict-aliasing violation) all make the whole execution undefined. Optimizers assume UB never happens, so a check such as x + 1 < x on a signed int can be compiled to false. Test with -fsanitize=address,undefined and separately with -fsanitize=thread; use std::memcpy or C++20 std::bit_cast for type punning.

Atomics. std::atomic operations default to memory_order_seq_cst, a single total order that is easiest to reason about. A release store and an acquire load that reads the stored value create a happens-before edge: everything written before the store is visible after the load. memory_order_relaxed guarantees atomicity and a per-variable modification order but nothing about other memory, which suits counters and statistics. volatile is not a synchronization mechanism in C++.

std::vector<int> v{1, 2, 3};
int& first = v[0];
v.push_back(4);                 // may reallocate: first may now dangle
std::string s = "long enough to be heap-allocated by std::string";
std::string_view sv = s + "!";  // views a temporary destroyed at the semicolon

Common misconceptions

  • "std::move moves the object." It is a cast. If no move constructor is viable, as with a const source, the copy constructor runs.
  • "Undefined behavior means a crash or a garbage value." The standard imposes no requirements; optimized builds may delete checks or reorder code, and a program can appear to work until a compiler upgrade.
  • "shared_ptr makes sharing thread-safe." Only the reference count is synchronized. Access to the pointee, and concurrent writes to one shared_ptr variable, still need a mutex or an atomic.
  • "References cannot dangle." A reference is only as valid as the object it names; returning one to a local or holding one across a reallocation is undefined behavior.
  • "volatile works for flags shared between threads." It provides neither atomicity nor inter-thread ordering; use std::atomic with release/acquire, or a mutex.

How to approach it

Compile small programs instead of only reading: build each experiment with -Wall -Wextra -g -fsanitize=address,undefined and let the sanitizer name the bug. Graduate interviews typically check RAII, references versus pointers and container choice. Mid-level and low-level roles add move semantics, invalidation, ownership graphs and virtual destructors; performance-focused interviews may ask you to justify a memory ordering or explain why an overflow check vanished at -O2. In an interview, state ownership aloud: "the cache owns its nodes; the index holds non-owning iterators".

Suggested order

  1. 1Read the cppreference RAII and Lifetime pages (temporary object lifetime) and Core Guidelines R.1 and F.43; write three programs that dangle (a returned local, a string_view into a temporary, a reference held across vector::push_back) and run them under AddressSanitizer.
  2. 2Read cppreference std::move and Move constructors plus Core Guidelines C.20, C.21, C.64 and ES.56; implement an owning buffer class, then print from each special member to see when vector reallocation copies (move not noexcept) versus moves.
  3. 3Read the Iterator invalidation table on the cppreference Containers library page; write a loop that erases matching elements from a vector and from an unordered_map correctly.
  4. 4Read cppreference unique_ptr, shared_ptr and weak_ptr and Core Guidelines R.21; build a tree with unique_ptr children and raw or weak_ptr parent links, then show the leak when the back edge is a shared_ptr by printing use_count and destructor calls.
  5. 5Read Core Guidelines C.35 and ES.63 and the cppreference virtual function page; delete a derived object through a base pointer without a virtual destructor, and slice an object by passing it by value; explain both outcomes.
  6. 6Read cppreference Undefined behavior (signed overflow, out-of-bounds access, UB and optimization) and the UndefinedBehaviorSanitizer page; compile an x + 1 < x overflow check at -O0 and -O2, compare results, then run it under -fsanitize=undefined.
  7. 7Read the relaxed and release-acquire sections of cppreference std::memory_order and Core Guidelines CP.2 and CP.8; implement a flag-protected handoff between two threads, first with a plain bool and then with release/acquire atomics, and run both under ThreadSanitizer.
  8. 8Solve LRU Cache in C++ with std::list and an unordered_map of list iterators, explaining why the iterators stay valid; then solve Print FooBar Alternately with std::mutex and std::condition_variable.

Step 2: Check off what you can do

Tap each one you can genuinely do or explain. Each earns 10 XP.

Step 3: Go deeper

  • C++ Core GuidelinesStart here

    Standard C++ Foundation (isocpp.github.io)

    Section: R: Resource management

    Ownership and lifetime rules (R.1, R.21, F.43), class rules for moves and destructors (C.20, C.21, C.35, C.64), ES.56 and ES.63, and CP.2 and CP.8 for concurrency; read the listed rules, not the whole document.

    Open(opens in a new tab)Link responded, 5 Oct 2026
  • cppreference.com: C++ referenceStart here

    cppreference.com (community-maintained reference)

    Section: RAII

    Precise pages for RAII, lifetime, value categories, move constructors, std::move, smart pointers, the iterator invalidation table, virtual functions, undefined behavior and std::memory_order.

    Open(opens in a new tab)Link responded, 5 Oct 2026
  • Clang sanitizers: UndefinedBehaviorSanitizer, AddressSanitizer and ThreadSanitizer

    LLVM Project (clang.llvm.org)

    Section: UndefinedBehaviorSanitizer

    Flags and coverage of UBSan, AddressSanitizer and ThreadSanitizer, used to run every experiment in this topic.

    Open(opens in a new tab)Link responded, 5 Oct 2026

Step 4: Practice

Use these to practice ownership and lifetime in C++, not new algorithms. Solve LRU Cache with std::list and an unordered_map of iterators and explain why list iterators survive moving nodes with splice; write Implement Trie with std::unique_ptr children so no manual delete is needed; in Copy List with Random Pointer, say who owns the new nodes; in Design HashMap, pick a per-bucket container and explain what rehashing invalidates. Solve the two concurrency problems with std::mutex, std::condition_variable and std::scoped_lock, and run your local copies under ThreadSanitizer. Extra exercises: write a move-only RAII wrapper for a POSIX file descriptor, and reproduce a shared_ptr cycle, then fix it with weak_ptr. For sustained practice, the CS144 and BusTub labs are substantial C++ codebases with tests; build them with sanitizers enabled.

Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).

Step 5: Test yourself

Answer each one out loud or on paper before you finish the lesson.

  1. Question 1What exactly does std::move do, and when does a "move" end up copying?
  2. Question 2Which operations invalidate references into a std::vector? Into a std::unordered_map?
  3. Question 3Why should move constructors be noexcept for types stored in a vector?
  4. Question 4When would you use weak_ptr, and what does lock() return once the object is gone?
  5. Question 5What happens when a derived object is passed by value to a function taking the base class?
  6. Question 6Why might the compiler remove a signed check such as x + 1 < x?
  7. Question 7What does memory_order_relaxed guarantee, and why is it insufficient for a ready flag that publishes data?

You’re done when you can

  • Identify the dangling reference or invalidated iterator in three unseen snippets and explain each in one or two sentences.
  • Write a move-only RAII class with noexcept move operations that runs cleanly under AddressSanitizer.
  • Explain without notes why deleting through a base pointer without a virtual destructor is undefined and what slicing loses.
  • Name five kinds of undefined behavior, which sanitizer (if any) catches each, and why an overflow check can disappear at -O2.
  • Implement a release/acquire handoff between two threads and state exactly which writes the consumer is guaranteed to see.

Finish the lesson

How confident do you feel?

Lesson complete!

+50 XP

Reviews will come up on the Practice tab so it sticks.

Your notes and study time

Log study time