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

Focus mode

C++ fundamentals: lifetime, ownership and undefined behavior

Read alongside

C++ Core Guidelines: 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.

Objectives

  • Explain RAII and object lifetime, and identify dangling references from returned locals, temporaries, string_view, lambda captures and invalidated iterators in unseen code.
  • Explain what std::move does and does not do, write a correct move constructor, and state why move operations should be noexcept.
  • Choose between unique_ptr, shared_ptr, weak_ptr and plain references or pointers for an ownership scenario, including breaking a shared_ptr cycle.
  • State the iterator and reference invalidation rules for vector, unordered_map, map and list, and predict when an insertion breaks existing iterators.
  • Explain virtual dispatch through vtables, why a polymorphic base needs a virtual destructor, and how object slicing happens.
  • Recognize signed overflow, out-of-bounds access, use after free, data races and strict-aliasing violations as undefined behavior and detect them with sanitizers.
  • Use std::atomic with seq_cst, release/acquire and relaxed ordering correctly for a statistic counter and a flag-protected data handoff.

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".

Self-check before you finish

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