1. What is the execution result of calling std::weak_ptr::lock()?
Answer: It returns a valid std::shared_ptr if the referenced object is still alive, or an empty std::shared_ptr if the object has been destroyed.
std::weak_ptr exists to observe an object managed by a shared_ptr without keeping it alive. That means the object can die while you still hold the weak_ptr — so you can’t just read through it safely. lock() is the safe access mechanism:
- The object is alive →
lock()atomically creates and returns a newshared_ptrto it (incrementing the refcount, so the object stays alive while you use it). - The object is already gone →
lock()returns an emptyshared_ptr(a null one).
The check is atomic with respect to destruction: if the object dies exactly when another thread calls lock(), the call either gets a valid pointer (the object stayed alive for it) or an empty one — never a dangling pointer.
std::weak_ptr<int> w = someShared;
if (std::shared_ptr<int> s = w.lock()) {
// object alive, s is safe to use
} else {
// object is gone
}
The interview answer: lock() returns a live shared_ptr or an empty/null shared_ptr if the object is destroyed — never a dangling pointer.
2. What compile-time optimization is enabled by Return Value Optimization (RVO / NRVO)?
Answer: The compiler constructs the returned object directly in the caller’s target memory, eliminating the copy/move constructor call entirely.
Normally, returning a value by value would be: function builds a local T, copies/moves it into a temporary, then copies/moves that into the caller’s variable. RVO (Return Value Optimization) lets the compiler skip the temporary and the copies: the function constructs the result directly into the address where the caller expects the final value.
T make() {
return T{...}; // constructed directly in caller's variable
}
T t = make(); // NO copy/move happens
NRVO (Named RVO) extends this to named locals: return local; where local is a local variable — the compiler can still elide the copy in many cases.
The upshot: returning big objects by value is often free — no copy, no move, just direct construction. This is why “return by value” is the idiomatic, efficient choice in modern C++.
The interview answer: RVO/NRVO constructs the return value in place in the caller’s memory, eliminating copy/move constructor calls.
3. What is the output of the following inheritance constructor sequence?
struct A {
A() { std::cout << "A"; }
};
struct B : A {
B() { std::cout << "B"; }
};
int main() {
B b;
}
Output: AB.
Object construction is bottom-up in the inheritance hierarchy: base class constructors run first, then derived. The base must be fully constructed (its invariants established) before the derived part can be initialized.
So for B b;:
A’s constructor runs first, printingA.- Then
B’s constructor body runs, printingB.
Output: AB.
The mirror rule applies to destruction, in reverse: derived destructor runs first, then base destructor (so the derived part is still intact when base cleanup runs). The interview answer: AB — base constructs first.
4. What is the purpose of std::scoped_lock introduced in C++17?
Answer: To lock multiple mutexes at once, deadlock-free, with RAII cleanup — one object, unlock on destruction.
Before C++17, locking several mutexes safely required std::lock(m1, m2) (which uses a deadlock-avoidance algorithm to acquire both without a deadlock race) plus a separate guard to manage unlocking. That split is easy to get wrong.
std::scoped_lock combines both: you hand it multiple mutexes and it locks them with the same deadlock-avoidance algorithm as std::lock, then unlocks all of them automatically when the lock object goes out of scope (RAII — safe under exceptions).
std::scoped_lock lock(m1, m2); // both locked, deadlock-free
// ... critical section ...
// destruction unlocks both
It also works with a single mutex, making it the drop-in successor to std::lock_guard. The interview answer: deadlock-free simultaneous locking of multiple mutexes via an RAII wrapper, with automatic unlock on scope exit.
5. What is the effect of applying const to a function parameter passed by value (void foo(const int x))?
Answer: It prevents x from being modified inside the function body, with no effect on how callers pass arguments.
By-value parameters receive a copy of the argument. The const here qualifies the local copy, not the caller’s original. The effects:
- Inside
foo,xis read-only — attemptingx = 42;is a compile error. - Callers are unaffected:
foo(5)andfoo(variable)both compile exactly as they would withoutconst. The caller’s variable is never modified either way (it was passed by value).
So const int x is purely a self-documentation and safety device for the function implementer — it signals “this parameter is an input only, I won’t mutate my local copy.” It does not change the function signature’s call syntax.
The interview answer: const on a value parameter restricts modification of the local copy inside the body; call sites are unchanged.
6. What makes std::atomic_ref (C++20) unique compared to standard std::atomic<T>?
Answer: It provides atomic operations on a non-atomic variable, without taking ownership of its storage.
std::atomic<T> owns the storage it operates on — you declare an atomic<int> and all access to that int goes through it. std::atomic_ref<T> is the opposite: you create a reference-like view over an existing plain variable, and use that view for atomic operations.
int x = 0; // ordinary, non-atomic variable
std::atomic_ref<int> ax(x); // atomic view over x
ax.fetch_add(1); // atomic read-modify-write on x
Why this matters: you can do plain, fast (non-atomic) operations on x during single-threaded phases of a program, then switch to atomic operations on the same storage during concurrent phases — with one underlying object. You can even create multiple atomic_refs to different members of a struct. It requires no copying of the storage and doesn’t change the object’s lifetime.
The interview answer: atomic_ref enables atomic operations on non-atomic variables via a temporary view, without owning the storage — plain and atomic access to the same object as needed.
7. What problem occurs if a base class destructor is NOT declared virtual when deleting a derived class object through a base class pointer?
Base* ptr = new Derived();
delete ptr;
Answer: Only the Base destructor runs — Derived’s destructor is never called — leaking resources owned by Derived and invoking undefined behavior.
delete works through the static type of the pointer you delete. If the base class destructor is non-virtual, the compiler has no way to dispatch to Derived’s destructor at runtime, so it calls Base::~Base() directly. Derived’s destructor (and any cleanup of members it allocated) is skipped — a resource leak, and per the standard, deleting through a non-virtual base destructor is undefined behavior.
struct Base { ~Base() {} }; // NON-virtual
struct Derived : Base { std::vector<int> v; };
Base* p = new Derived();
delete p; // Derived::~Derived() never runs → v leaks
The rule of thumb: any class intended as a base class should have a virtual destructor (and if it’s a polymorphic base with virtual functions, it virtually always should). If a class is never meant to be derived from, mark it final and keep the destructor non-virtual.
The interview answer: with a non-virtual base destructor, delete runs only Base::~Base(), skipping Derived cleanup and causing UB + leaks.
8. What is the evaluation order guarantee of function arguments in C++17?
Answer: Arguments are evaluated in un-interleaved but unspecified order — each argument is fully evaluated before the next begins, but the compiler may pick any order.
Pre-C++17, argument evaluation was completely unspecified and could be interleaved in pathological cases. C++17 tightened this: the evaluations of individual arguments can no longer be interleaved. So for f(a(), b(), c()):
a(),b(),c()each complete fully before the next starts — no partial interleaving.- But the order among them — a then b then c, or c then b then a — is still unspecified.
This matters for correctness: if the argument expressions have side effects or read/write shared state, you must not rely on any particular order. f(++i, i++) is still a bug. The guarantee just means each individual subexpression’s evaluation is atomic with respect to the others.
The interview answer: arguments are evaluated completely (non-interleaved), but their relative order remains unspecified.
9. What is the fundamental requirement for a class type to be safely manipulated inside a std::atomic<T> wrapper?
Answer: T must be Trivially Copyable (std::is_trivially_copyable_v<T>).
std::atomic<T> must be able to perform its operations with the hardware’s atomic primitives — which fundamentally operate on raw bytes: memcpy-style copying, compare-and-swap (CAS) instructions, and so on. That only works if the type’s representation is a plain bit pattern with no hidden semantics.
Trivially copyable requires (in essence):
- Trivially copyable/copy-assignable — copying is a plain
memcpy, no user logic. - Trivially destructible — destruction has no user-defined work.
- No virtual functions, no virtual base classes — a vtable pointer would make the bit pattern object-identity-bearing.
If T is non-trivially-copyable (has virtuals, custom copy constructors, user-defined destructors, etc.), copying it bitwise is wrong, and using it in std::atomic<T> is ill-formed (or the object simply can’t be represented for lock-free atomic ops).
The interview answer: T must be trivially copyable (no virtuals, trivial copy/destructor) so the atomic can operate on it with raw bitwise/CAS primitives.
10. What is the runtime effect of calling std::terminate()?
Answer: Program execution ends immediately, without stack unwinding — object destructors do not run.
std::terminate() is the runtime’s last-resort handler, invoked when exception handling has failed beyond recovery: an exception escapes a noexcept function, an exception is thrown during active unwinding, or an uncaught exception escapes main’s try/catch framework. It calls the registered terminate handler — by default std::abort() — which terminates the process.
The critical consequence: no stack unwinding is performed. Local objects’ destructors are not called. Anything relying on destructors for cleanup (file handles, mutexes, memory) is lost — which is exactly why a terminated program is a hard abort rather than a graceful shutdown.
The interview answer: std::terminate() aborts immediately (via std::abort() by default), skipping stack unwinding and destructor calls.
Premium Content
Unlock Top 25 - Part 2 and all premium lessons with a subscription.
From ₹199.99/year — See plans