Interview concept

C++ Concurrency, Memory Ownership, And RAII

Asked of: Software Engineer

Last updated

Vertical decision flowchart for C++ ownership and concurrency: choose unique_ptr/shared_ptr/weak_ptr/raw, or std::atomic vs std::mutex with lock_guard/unique_lock and deadlock avoidance, with RAII tip footer.

What's being tested

Interviewers probe whether you can reason about safe object lifetime, resource cleanup, and concurrent access in production C++ code. They expect you to demonstrate practical use of RAII for deterministic cleanup, correct ownership transfer using move semantics, and the right synchronization primitive choices (`std::mutex` vs `std::atomic`) to avoid data races and undefined behavior. For eBay-scale services, this maps to delivering robust, maintainable components (connection pools, caches, task queues) that don't leak, deadlock, or silently corrupt state under concurrency.

Core knowledge

  • RAII: Resource Acquisition Is Initialization guarantees deterministic cleanup via constructors/destructors; use it for file handles, locks, sockets, and transactions to avoid leaks and ensure exception-safety.

  • Ownership models: Prefer single ownership with `std::unique_ptr` for exclusive resources; use `std::shared_ptr` for shared ownership and `std::weak_ptr` to break cycles and allow non-owning observers.

  • Move semantics: Implement/consume move constructors and operator= to transfer ownership cheaply; moving invalidates the source but avoids deep copies and double-free errors.

  • Lifetime vs pointer validity: Raw pointers are non-owning; never extend lifetime beyond the owning smart-pointer scope. Dangling pointer bugs often occur across thread boundaries without explicit synchronization.

  • Data race definition: Concurrent unsynchronized accesses (one is a write) to the same memory location cause a data race and undefined behavior; use `std::atomic` or locks to avoid it.

  • Mutex primitives: Use `std::mutex` with `std::lock_guard` for simple scope-based locking and `std::unique_lock` when you need deferred/unlocked locking or `std::condition_variable` waits.

  • Atomics and memory ordering: `std::atomic<T>` provides lock-free primitives where appropriate; understand memory_order_relaxed, memory_order_acquire, memory_order_release, and acquire-release pairs to reason about visibility (happens-before).

  • Compare-and-swap: Use `compare_exchange_strong`/`weak` carefully for lock-free updates; watch for the ABA problem when reusing memory without versioning.

  • Deadlock avoidance: Enforce a global lock order, prefer finer-grained locks, and consider `std::scoped_lock` for multiple locks; detect possible lock inversions during design.

  • Double-checked locking caveats: Correct only with proper synchronization and memory barriers; on weakly ordered architectures, naive double-checked locking is unsafe without `std::atomic` with acquire/release semantics.

  • Reference counting costs: `std::shared_ptr` has atomic reference updates by default; expect contention and cost in hot paths — measure before choosing shared ownership for high-throughput code.

  • Testing and tooling: Use thread sanitizers (`-fsanitize=thread`), address sanitizers, and static analyzers to catch UB, data races, and lifetime issues early.

Worked example — "Implement a thread-safe cache using RAII and smart pointers"

First 30s: clarify expected semantics — is the cache bounded? Are values immutable once inserted? What concurrency guarantees (concurrent reads, single-writer) are required? Assume a bounded cache with concurrent readers and occasional writers; values are immutable after construction. Skeleton: (1) store entries as `std::shared_ptr<Value>` in a `std::unordered_map<Key, std::shared_ptr<Value>>`, (2) protect the map with a `std::mutex` and `std::lock_guard` for map mutations, (3) return `std::shared_ptr` to callers so lifetime extends outside the lock (RAII ensures cleanup). Tradeoff: this is simple and safe but `std::shared_ptr` incurs atomic refcount overhead on hot reads; alternative is read-write lock or lock-striping to improve read throughput. Flag explicit decisions: if readers vastly outnumber writers, consider `std::shared_mutex` for shared locking or a concurrent hashmap implementation. Close: if more time, I'd add eviction policy (LRU) with consistent ordering under concurrency and benchmark the `shared_ptr` vs copy semantics for hot values.

A second angle — "Design a producer-consumer queue with minimal latency"

Same building blocks (RAII for locks, correct ownership transfer), but constraints shift: low latency and high throughput favor lock-free or wait-free structures. Use a ring buffer with `std::atomic<size_t>` head/tail indices and `std::memory_order_*` for visibility; store elements in slots as `std::unique_ptr<T>` to avoid copies and ensure deterministic destruction. Clarify whether multiple producers/consumers are allowed — single-producer/single-consumer permits much simpler and faster algorithms. Call out tradeoffs: lock-free designs reduce blocking but increase complexity (ABA, false sharing), so use them only when profiling shows lock contention is the bottleneck.

Common pitfalls

Pitfall: Pretending `std::shared_ptr` makes your code automatically thread-safe. `std::shared_ptr` protects its reference count atomically, but the pointed-to object still requires synchronization for concurrent mutation; copying a `shared_ptr` is safe, modifying the pointee is not.

Pitfall: Returning a raw pointer to an object owned by a local smart pointer. A common mistake is returning `raw_ptr.get()` from a function while the `shared_ptr` dies at scope exit, producing a dangling pointer and UB. Return a smart pointer or ensure caller owns lifetime.

Pitfall: Mixing locks and atomics without a clear happens-before model. Using `std::atomic` for some flags and `std::mutex` for related data can leave a gap where readers observe stale or inconsistent state unless acquire-release ordering or explicit locking is used to establish happens-before.

Connections

Interviewers may pivot to lock-free algorithms and hazard pointers, or to the C++ memory model details (sequenced-before, happens-before). They may also ask about testing and observability: using sanitizers, profiling refcount hot spots, or designing metrics for concurrency contention (`lock hold time`, `wait count`).

Further reading

Related concepts