1. What occurs when two objects holding std::shared_ptr instances reference each other, forming a circular dependency?
Answer: A memory leak — both reference counts stay at least 1 forever, so neither object is ever deleted.
std::shared_ptr manages lifetime with a reference count: the object is destroyed when the count drops to zero. A cycle breaks that mechanism. If A holds a shared_ptr to B and B holds a shared_ptr to A, then:
- A’s count is at least 1 (B references it).
- B’s count is at least 1 (A references it).
Even when every external pointer goes out of scope, the two objects keep each other alive by mutual reference. The count never reaches zero, so neither destructor runs. That’s a leak — the classic reference-counting cycle problem (also why Python’s GC needs a cycle collector).
The fix is to break the cycle with std::weak_ptr: one link in the cycle is a non-owning weak reference that doesn’t increment the count. Typically the “owner” uses shared_ptr and the “back-pointer” uses weak_ptr. When the owning side drops away, the count hits zero and everything is cleaned up — the weak_ptr simply expires.
The interview answer: circular shared_ptr references leak; break cycles with weak_ptr.
Answer:
A memory leak — both reference counts stay at least 1 forever, so neither object is ever deleted.
std::shared_ptr manages lifetime with a reference count: the object is destroyed when the count drops to zero. A cycle breaks that mechanism. If A holds a shared_ptr to B and B holds a shared_ptr to A, then:
- A’s count is at least 1 (B references it).
- B’s count is at least 1 (A references it).
Even when every external pointer goes out of scope, the two objects keep each other alive by mutual reference. The count never reaches zero, so neither destructor runs. That’s a leak — the classic reference-counting cycle problem (also why Python’s GC needs a cycle collector).
The fix is to break the cycle with std::weak_ptr: one link in the cycle is a non-owning weak reference that doesn’t increment the count. Typically the “owner” uses shared_ptr and the “back-pointer” uses weak_ptr. When the owning side drops away, the count hits zero and everything is cleaned up — the weak_ptr simply expires.
The interview answer: circular shared_ptr references leak; break cycles with weak_ptr.
2. According to the C++ Strict Aliasing Rule, which pointer dereference pattern results in Undefined Behavior?
Answer: Reinterpreting a float object by casting its address to an int* and dereferencing it.
The strict aliasing rule says: you may only access an object’s storage through a pointer of a compatible type. Accessing a float as an int violates this — the compiler is allowed to assume two objects of different incompatible types don’t overlap, and it optimizes on that assumption. When you break the rule, you get undefined behavior: the optimized code may behave unexpectedly.
The exception list — types you may alias through:
- Character types —
char*,signed char*,unsigned char*can examine the bytes of any object. std::byte*— the byte-aliasing exception.- Signed/unsigned variants of the same type.
- Base/derived related types.
Everything else — like float accessed as int — is off-limits. This is why reinterpret_cast<float*>(&intVar) then dereferencing is a bug, and why the correct way to inspect an object’s bytes is to cast to char*/std::byte* (or memcpy).
The interview answer: dereferencing a float through an int* violates strict aliasing and is undefined behavior; byte access must go through char/std::byte pointers.
Answer:
Reinterpreting a float object by casting its address to an int* and dereferencing it.
The strict aliasing rule says: you may only access an object’s storage through a pointer of a compatible type. Accessing a float as an int violates this — the compiler is allowed to assume two objects of different incompatible types don’t overlap, and it optimizes on that assumption. When you break the rule, you get undefined behavior: the optimized code may behave unexpectedly.
The exception list — types you may alias through:
- Character types —
char*,signed char*,unsigned char*can examine the bytes of any object. std::byte*— the byte-aliasing exception.- Signed/unsigned variants of the same type.
- Base/derived related types.
Everything else — like float accessed as int — is off-limits. This is why reinterpret_cast<float*>(&intVar) then dereferencing is a bug, and why the correct way to inspect an object’s bytes is to cast to char*/std::byte* (or memcpy).
The interview answer: dereferencing a float through an int* violates strict aliasing and is undefined behavior; byte access must go through char/std::byte pointers.
3. What is required when using placement new to construct an object in pre-allocated memory?
alignas(T) char buffer[sizeof(T)];
T* ptr = new (buffer) T();
Answer: Call the destructor explicitly — ptr->~T() — and manage the buffer separately. Never use delete ptr;.
Placement new (new (buffer) T()) constructs an object inside memory you already own — a stack buffer, a pool, or some other pre-allocated region. It performs no heap allocation of its own. That distinction changes cleanup completely.
Because the memory was not allocated by the global new, calling delete ptr; is undefined behavior — delete would try to free memory it doesn’t own. The correct teardown is the reverse of construction:
- Call the destructor manually:
ptr->~T();— this runs cleanup of any resources the object acquired (like a normal destructor would). - Free the buffer however it was meant to be freed — if the buffer is heap-allocated (
new[]/malloc), free it with the matching deallocator; if it’s a stack buffer, nothing to free.
Placement new is the tool for custom memory pools, arena allocators, and embedded contexts where you want object lifetime decoupled from raw memory lifetime. The interview answer: manual ptr->~T() plus separate buffer management; delete ptr is UB.
Answer:
Call the destructor explicitly — ptr->~T() — and manage the buffer separately. Never use delete ptr;.
Placement new (new (buffer) T()) constructs an object inside memory you already own — a stack buffer, a pool, or some other pre-allocated region. It performs no heap allocation of its own. That distinction changes cleanup completely.
Because the memory was not allocated by the global new, calling delete ptr; is undefined behavior — delete would try to free memory it doesn’t own. The correct teardown is the reverse of construction:
- Call the destructor manually:
ptr->~T();— this runs cleanup of any resources the object acquired (like a normal destructor would). - Free the buffer however it was meant to be freed — if the buffer is heap-allocated (
new[]/malloc), free it with the matching deallocator; if it’s a stack buffer, nothing to free.
Placement new is the tool for custom memory pools, arena allocators, and embedded contexts where you want object lifetime decoupled from raw memory lifetime. The interview answer: manual ptr->~T() plus separate buffer management; delete ptr is UB.
4. 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.
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.
5. 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.
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.
6. What is the behavior of std::array compared to standard C-style arrays?
Answer: std::array wraps a fixed-size C-style array into a stack-allocated container that knows its length and supports standard iterators and value copy-assignment.
std::array<T, N> is a thin, zero-overhead wrapper around T[N]. Unlike a raw C array, it:
- Knows its size —
.size()returnsN; no moresizeof(arr)/sizeof(arr[0])hacks. - Passes by value properly — raw arrays decay to pointers when passed to functions;
std::arrayis a real type that can be copied, assigned, and returned by value. - Works with the STL —
.begin()/.end(), range-for, and standard algorithms all work directly. - Offers safety when asked —
.at(i)does bounds-checked access (throwsstd::out_of_range), whileoperator[]stays unchecked like a raw array.
And critically, it has zero abstraction overhead: it’s a stack array with no dynamic allocation, laid out identically to T[N]. The interview answer: a fixed-size stack container wrapping T[N] that knows its length, supports value semantics and STL algorithms, with no heap allocation.
Answer:
std::array wraps a fixed-size C-style array into a stack-allocated container that knows its length and supports standard iterators and value copy-assignment.
std::array<T, N> is a thin, zero-overhead wrapper around T[N]. Unlike a raw C array, it:
- Knows its size —
.size()returnsN; no moresizeof(arr)/sizeof(arr[0])hacks. - Passes by value properly — raw arrays decay to pointers when passed to functions;
std::arrayis a real type that can be copied, assigned, and returned by value. - Works with the STL —
.begin()/.end(), range-for, and standard algorithms all work directly. - Offers safety when asked —
.at(i)does bounds-checked access (throwsstd::out_of_range), whileoperator[]stays unchecked like a raw array.
And critically, it has zero abstraction overhead: it’s a stack array with no dynamic allocation, laid out identically to T[N]. The interview answer: a fixed-size stack container wrapping T[N] that knows its length, supports value semantics and STL algorithms, with no heap allocation.
7. What is the role of std::launder introduced in C++17?
Answer: It tells the compiler that a new object now lives at an address that previously held another object, defeating invalid optimizer assumptions based on pointer identity.
std::launder is for a subtle lifetime problem. Consider a member function that returns a pointer to its own object, where the object was reconstructed at the same address via placement new. The compiler may legally assume the pointer refers to the original object (pointer identity/aliasing invariants). After placement-new replaces the object, that assumption is invalid — yet the compiler doesn’t know.
T* p = new (mem) T(); // construct new T at old address
return std::launder(p); // "trust me, this is the new object"
std::launder produces a pointer to the new object at that address, suppressing the stale assumptions. It’s mainly needed in advanced scenarios: reusing storage for a new object, type punning in optional-like containers, or returning this from a method where the object was recreated. For ordinary code you rarely need it. The interview answer: std::launder yields a valid pointer to an object newly constructed at an address previously occupied by another object, defeating stale optimization assumptions.
Answer:
It tells the compiler that a new object now lives at an address that previously held another object, defeating invalid optimizer assumptions based on pointer identity.
std::launder is for a subtle lifetime problem. Consider a member function that returns a pointer to its own object, where the object was reconstructed at the same address via placement new. The compiler may legally assume the pointer refers to the original object (pointer identity/aliasing invariants). After placement-new replaces the object, that assumption is invalid — yet the compiler doesn’t know.
T* p = new (mem) T(); // construct new T at old address
return std::launder(p); // "trust me, this is the new object"
std::launder produces a pointer to the new object at that address, suppressing the stale assumptions. It’s mainly needed in advanced scenarios: reusing storage for a new object, type punning in optional-like containers, or returning this from a method where the object was recreated. For ordinary code you rarely need it. The interview answer: std::launder yields a valid pointer to an object newly constructed at an address previously occupied by another object, defeating stale optimization assumptions.
8. What is the evaluation result of alignof(std::max_align_t)?
Answer: The maximum alignment requirement supported by the platform for scalar types — typically 8 or 16 bytes.
std::max_align_t is a special scalar type whose alignment requirement is at least as strict as every other scalar type on the platform. Taking alignof(std::max_align_t) therefore reports the largest scalar alignment the architecture supports — commonly 8 bytes (32-bit) or 16 bytes (64-bit with long double). It’s essentially the answer to “what alignment do I need for any scalar?”
It matters because malloc/operator new are required to return memory aligned suitably for std::max_align_t, which is why heap allocations work for any scalar type. The interview answer: the platform’s maximum scalar alignment (typically 8 or 16 bytes), used as the baseline guarantee for heap allocations.
Answer:
The maximum alignment requirement supported by the platform for scalar types — typically 8 or 16 bytes.
std::max_align_t is a special scalar type whose alignment requirement is at least as strict as every other scalar type on the platform. Taking alignof(std::max_align_t) therefore reports the largest scalar alignment the architecture supports — commonly 8 bytes (32-bit) or 16 bytes (64-bit with long double). It’s essentially the answer to “what alignment do I need for any scalar?”
It matters because malloc/operator new are required to return memory aligned suitably for std::max_align_t, which is why heap allocations work for any scalar type. The interview answer: the platform’s maximum scalar alignment (typically 8 or 16 bytes), used as the baseline guarantee for heap allocations.
9. What happens if std::vector::reserve() is called with a capacity less than the vector’s current capacity()?
Answer: The call is safely ignored — capacity stays unchanged, elements and iterators untouched.
reserve(n) only grows: it reallocates only if n is greater than the current capacity. If n <= capacity(), nothing happens — no reallocation, no invalidation, no shrink. The request is a no-op.
To actually shrink, you use shrink_to_fit() (a non-binding request — the implementation may or may not reduce capacity). The contrast is important:
reserve(n)— “make capacity at least n” (never shrinks).shrink_to_fit()— “make capacity fit size” (best-effort, non-binding).
The interview answer: reserve(n) with n <= capacity() is ignored, leaving capacity, elements, and iterators unchanged.
Answer:
The call is safely ignored — capacity stays unchanged, elements and iterators untouched.
reserve(n) only grows: it reallocates only if n is greater than the current capacity. If n <= capacity(), nothing happens — no reallocation, no invalidation, no shrink. The request is a no-op.
To actually shrink, you use shrink_to_fit() (a non-binding request — the implementation may or may not reduce capacity). The contrast is important:
reserve(n)— “make capacity at least n” (never shrinks).shrink_to_fit()— “make capacity fit size” (best-effort, non-binding).
The interview answer: reserve(n) with n <= capacity() is ignored, leaving capacity, elements, and iterators unchanged.
10. What is the key performance advantage of std::make_shared over constructing std::shared_ptr with a raw pointer?
Answer: std::make_shared performs a single, contiguous allocation for both the managed object and the control block — fewer allocations and better cache locality.
std::shared_ptr<T>(new T()) does two heap allocations: one for T, one for the control block (refcounts, etc.) — which sit in separate memory. std::make_shared<T>() does one allocation big enough for both the object and the control block together, contiguous in memory.
Benefits:
- Fewer allocations = less overhead and less fragmentation.
- Object and control block are adjacent → better cache locality when bumping refcounts.
- Exception-safety:
make_sharedavoids the classic leak if the argument evaluation throws.
The trade-off is the weak_ptr problem (next question): the combined block can’t be freed until weak references are also gone. The interview answer: one contiguous allocation for object + control block, reducing allocation count and improving locality.
Answer:
std::make_shared performs a single, contiguous allocation for both the managed object and the control block — fewer allocations and better cache locality.
std::shared_ptr<T>(new T()) does two heap allocations: one for T, one for the control block (refcounts, etc.) — which sit in separate memory. std::make_shared<T>() does one allocation big enough for both the object and the control block together, contiguous in memory.
Benefits:
- Fewer allocations = less overhead and less fragmentation.
- Object and control block are adjacent → better cache locality when bumping refcounts.
- Exception-safety:
make_sharedavoids the classic leak if the argument evaluation throws.
The trade-off is the weak_ptr problem (next question): the combined block can’t be freed until weak references are also gone. The interview answer: one contiguous allocation for object + control block, reducing allocation count and improving locality.
11. What occurs if std::shared_ptr objects are created using std::make_shared when an object holds weak references long after its lifetime ends?
Answer: The object’s memory can’t be deallocated until all std::weak_ptr instances are destroyed, because the object and control block share one allocation.
With std::make_shared, the object and control block live in a single allocation. The control block must survive as long as there are any references — strong or weak — because it holds the weak count and is what weak pointers look up to check if the object is alive.
So the timeline is:
- Strong refcount hits 0 → the object’s destructor runs (resources released,
shared_ptrusers can’t access it). - But the raw memory stays allocated because the control block still needs to exist for the surviving
weak_ptrs to query.
Only when the last weak_ptr is destroyed does the whole combined block get freed. The cost: with make_shared, memory is held longer if weak pointers outlive the object. The interview answer: the shared allocation persists until all weak_ptrs die — object destructed at strong-count 0, but memory freed only at weak-count 0.
Answer:
The object’s memory can’t be deallocated until all std::weak_ptr instances are destroyed, because the object and control block share one allocation.
With std::make_shared, the object and control block live in a single allocation. The control block must survive as long as there are any references — strong or weak — because it holds the weak count and is what weak pointers look up to check if the object is alive.
So the timeline is:
- Strong refcount hits 0 → the object’s destructor runs (resources released,
shared_ptrusers can’t access it). - But the raw memory stays allocated because the control block still needs to exist for the surviving
weak_ptrs to query.
Only when the last weak_ptr is destroyed does the whole combined block get freed. The cost: with make_shared, memory is held longer if weak pointers outlive the object. The interview answer: the shared allocation persists until all weak_ptrs die — object destructed at strong-count 0, but memory freed only at weak-count 0.
12. What does the std::align function calculate?
Answer: It fits an object of a given size and alignment into a raw buffer, adjusting the buffer pointer and remaining space accordingly.
std::align(alignment, size, ptr, space) is the manual memory-alignment helper used by custom allocators and pools. Given a buffer (ptr, space):
- If there’s room, it advances
ptrto the first address that satisfies the requested alignment (rounding up), decreasesspaceby the bytes consumed, and returnstrue. - If the remaining space is too small to fit
sizeat the required alignment, it returnsfalseand leaves things unchanged.
This is how you hand out aligned chunks from a raw byte pool — e.g., allocating aligned storage for a type inside a custom arena. The interview answer: it aligns a pointer into a buffer for a given size/alignment, updating the pointer and remaining space, or reports failure.
Answer:
It fits an object of a given size and alignment into a raw buffer, adjusting the buffer pointer and remaining space accordingly.
std::align(alignment, size, ptr, space) is the manual memory-alignment helper used by custom allocators and pools. Given a buffer (ptr, space):
- If there’s room, it advances
ptrto the first address that satisfies the requested alignment (rounding up), decreasesspaceby the bytes consumed, and returnstrue. - If the remaining space is too small to fit
sizeat the required alignment, it returnsfalseand leaves things unchanged.
This is how you hand out aligned chunks from a raw byte pool — e.g., allocating aligned storage for a type inside a custom arena. The interview answer: it aligns a pointer into a buffer for a given size/alignment, updating the pointer and remaining space, or reports failure.
13. What will happen if you attempt to copy a std::unique_ptr using standard copy assignment (p1 = p2;)?
Answer: Compilation fails — std::unique_ptr deletes its copy constructor and copy assignment operator.
std::unique_ptr enforces exclusive ownership, so copying it is meaningless (who owns the pointee? a copy would mean two owners). The copy constructor and copy assignment operator are explicitly deleted (= delete), so p1 = p2; is a compile-time error, not a runtime problem.
Ownership can only be transferred via move: p1 = std::move(p2);, which hands the pointee to p1 and leaves p2 null. The interview answer: it’s a compile-time error; copying is deleted, so you must std::move.
Answer:
Compilation fails — std::unique_ptr deletes its copy constructor and copy assignment operator.
std::unique_ptr enforces exclusive ownership, so copying it is meaningless (who owns the pointee? a copy would mean two owners). The copy constructor and copy assignment operator are explicitly deleted (= delete), so p1 = p2; is a compile-time error, not a runtime problem.
Ownership can only be transferred via move: p1 = std::move(p2);, which hands the pointee to p1 and leaves p2 null. The interview answer: it’s a compile-time error; copying is deleted, so you must std::move.
14. What is the main reason to prefer std::make_unique over using new directly (std::unique_ptr<T>(new T()))?
Answer: std::make_unique is exception-safe — it prevents memory leaks during function-call argument evaluation.
Pre-C++17, evaluation order of function arguments was unspecified. Consider:
foo(std::unique_ptr<T>(new T()), someOtherThrowingFunction());
The compiler could evaluate new T() first, then call the throwing function before the unique_ptr constructor takes ownership of the raw pointer. If it throws, new T()’s memory is leaked. std::make_unique<T>() wraps allocation + construction into one exception-safe operation — there’s no window where a raw pointer exists unprotected. (C++17’s evaluation-order fixes reduced this, but make_unique remains the clean idiom.) Bonus: also makes the code shorter. The interview answer: make_unique is exception-safe, removing the leak window during argument evaluation.
Answer:
std::make_unique is exception-safe — it prevents memory leaks during function-call argument evaluation.
Pre-C++17, evaluation order of function arguments was unspecified. Consider:
foo(std::unique_ptr<T>(new T()), someOtherThrowingFunction());
The compiler could evaluate new T() first, then call the throwing function before the unique_ptr constructor takes ownership of the raw pointer. If it throws, new T()’s memory is leaked. std::make_unique<T>() wraps allocation + construction into one exception-safe operation — there’s no window where a raw pointer exists unprotected. (C++17’s evaluation-order fixes reduced this, but make_unique remains the clean idiom.) Bonus: also makes the code shorter. The interview answer: make_unique is exception-safe, removing the leak window during argument evaluation.
15. What is the primary function of std::enable_shared_from_this?
Answer: It lets a class method safely produce a std::shared_ptr to this sharing the existing control block instead of creating a duplicate.
The naive approach — a member returning std::shared_ptr<T>(this) — creates a brand-new control block for the same object. Now two independent control blocks believe they own the same T; when both reach refcount 0, you get a double free.
The fix: inherit from std::enable_shared_from_this<T> and call shared_from_this():
struct Node : std::enable_shared_from_this<Node> {
std::shared_ptr<Node> getShared() {
return shared_from_this(); // shares the existing control block
}
};
auto p = std::make_shared<Node>();
auto q = p->getShared(); // same control block, safe
Requirement: the object must be owned by a shared_ptr before calling shared_from_this() (otherwise it throws std::bad_weak_ptr). The interview answer: shared_from_this() returns a shared_ptr sharing the object’s existing control block, avoiding double-free.
Answer:
It lets a class method safely produce a std::shared_ptr to this sharing the existing control block instead of creating a duplicate.
The naive approach — a member returning std::shared_ptr<T>(this) — creates a brand-new control block for the same object. Now two independent control blocks believe they own the same T; when both reach refcount 0, you get a double free.
The fix: inherit from std::enable_shared_from_this<T> and call shared_from_this():
struct Node : std::enable_shared_from_this<Node> {
std::shared_ptr<Node> getShared() {
return shared_from_this(); // shares the existing control block
}
};
auto p = std::make_shared<Node>();
auto q = p->getShared(); // same control block, safe
Requirement: the object must be owned by a shared_ptr before calling shared_from_this() (otherwise it throws std::bad_weak_ptr). The interview answer: shared_from_this() returns a shared_ptr sharing the object’s existing control block, avoiding double-free.
16. What is the behavior of calling std::vector::emplace_back instead of push_back?
Answer: emplace_back constructs the element in place inside the vector’s storage from its arguments, avoiding a separate temporary and copy/move.
push_back(x) takes an already-constructed element and copies or moves it into the vector. emplace_back(args...) forwards its arguments directly to the element’s constructor, building the element inside the vector’s buffer:
v.push_back(Person("Alice", 30)); // construct temp → move into vector
v.emplace_back("Alice", 30); // construct Person directly in place
With emplace_back, no temporary Person is created and no move/copy is needed — the element’s constructor runs exactly once, in the vector’s memory. This is both faster (for non-trivial types) and expresses intent (you’re constructing, not inserting an existing object). The interview answer: emplace_back forwards arguments to the element constructor, constructing directly in place and avoiding copy/move of a temporary.
Answer:
emplace_back constructs the element in place inside the vector’s storage from its arguments, avoiding a separate temporary and copy/move.
push_back(x) takes an already-constructed element and copies or moves it into the vector. emplace_back(args...) forwards its arguments directly to the element’s constructor, building the element inside the vector’s buffer:
v.push_back(Person("Alice", 30)); // construct temp → move into vector
v.emplace_back("Alice", 30); // construct Person directly in place
With emplace_back, no temporary Person is created and no move/copy is needed — the element’s constructor runs exactly once, in the vector’s memory. This is both faster (for non-trivial types) and expresses intent (you’re constructing, not inserting an existing object). The interview answer: emplace_back forwards arguments to the element constructor, constructing directly in place and avoiding copy/move of a temporary.
17. What happens when calling std::shared_ptr::reset() on a non-empty smart pointer?
Answer: It decrements the shared reference count, destroys the managed object if the count hits zero, and leaves the shared_ptr empty (null).
reset() releases ownership of the current resource:
- Decrement the strong reference count.
- If the count reaches zero, the managed object is destroyed (and, with the default deleter, its memory freed).
- The
shared_ptris set to a null/empty state (it now owns nothing).
If other shared_ptrs still reference the object, the object survives (count > 0); only this pointer’s ownership is dropped. reset() is the manual “let go” that destructors perform automatically. The interview answer: reset() drops ownership, decrementing the refcount and destroying the object if it hits zero, leaving the pointer null.
Answer:
It decrements the shared reference count, destroys the managed object if the count hits zero, and leaves the shared_ptr empty (null).
reset() releases ownership of the current resource:
- Decrement the strong reference count.
- If the count reaches zero, the managed object is destroyed (and, with the default deleter, its memory freed).
- The
shared_ptris set to a null/empty state (it now owns nothing).
If other shared_ptrs still reference the object, the object survives (count > 0); only this pointer’s ownership is dropped. reset() is the manual “let go” that destructors perform automatically. The interview answer: reset() drops ownership, decrementing the refcount and destroying the object if it hits zero, leaving the pointer null.
18. What is the alignment requirement of a class struct containing char a; double b; int c; on standard 64-bit x86 systems (assuming default packing)?
Answer: 8-byte alignment, with the total struct size padded to a multiple of 8 — 24 bytes total.
Struct alignment is driven by the member with the strictest alignment. double requires 8-byte alignment (on standard 64-bit x86), so the struct aligns to 8.
Layout with padding:
char a— offset 0 (1 byte), then 7 bytes padding.double b— offset 8 (8 bytes, ends at 16).int c— offset 16 (4 bytes, ends at 20).- Padding to struct alignment: total must be a multiple of 8 → pad 4 bytes → 24 bytes.
That’s why the members don’t pack tightly (the naive “13 bytes”) — the compiler inserts padding so every member is at its naturally aligned address. The interview answer: 8-byte alignment (from double), 24 bytes total after padding.
Answer:
8-byte alignment, with the total struct size padded to a multiple of 8 — 24 bytes total.
Struct alignment is driven by the member with the strictest alignment. double requires 8-byte alignment (on standard 64-bit x86), so the struct aligns to 8.
Layout with padding:
char a— offset 0 (1 byte), then 7 bytes padding.double b— offset 8 (8 bytes, ends at 16).int c— offset 16 (4 bytes, ends at 20).- Padding to struct alignment: total must be a multiple of 8 → pad 4 bytes → 24 bytes.
That’s why the members don’t pack tightly (the naive “13 bytes”) — the compiler inserts padding so every member is at its naturally aligned address. The interview answer: 8-byte alignment (from double), 24 bytes total after padding.
19. What is the purpose of std::span introduced in C++20?
Answer: A non-owning, contiguous view over a sequence of objects — raw arrays, std::vector, std::array — with no copying or allocation.
std::span<T> holds a pointer and a count into a contiguous range. It’s the “buffer view” for arrays the way string_view is the view for strings:
- Non-owning — it never allocates, never copies the underlying data, never frees anything.
- Any contiguous source — construct it from C arrays,
std::vector,std::array, or a pointer+size. - Bounds-aware API —
.size(),.front(),.back(), iteration, and.subspan()for slicing.
It’s ideal for function parameters that just want “give me a contiguous bunch of T’s” without caring about the container type. The interview answer: a non-owning pointer+length view over any contiguous sequence, enabling zero-copy, container-agnostic access.
Answer:
A non-owning, contiguous view over a sequence of objects — raw arrays, std::vector, std::array — with no copying or allocation.
std::span<T> holds a pointer and a count into a contiguous range. It’s the “buffer view” for arrays the way string_view is the view for strings:
- Non-owning — it never allocates, never copies the underlying data, never frees anything.
- Any contiguous source — construct it from C arrays,
std::vector,std::array, or a pointer+size. - Bounds-aware API —
.size(),.front(),.back(), iteration, and.subspan()for slicing.
It’s ideal for function parameters that just want “give me a contiguous bunch of T’s” without caring about the container type. The interview answer: a non-owning pointer+length view over any contiguous sequence, enabling zero-copy, container-agnostic access.
20. What causes dangling pointers when using std::vector?
Answer: Capacity growth reallocates the internal buffer, moving elements to new memory and freeing the old buffer — invalidating all existing pointers, references, and iterators.
When push_back/insert exceed the vector’s current capacity, the vector allocates a new, larger heap block, moves (or copies) the elements over, and frees the old block. Any pointer, reference, or iterator that pointed into the old block now points at freed memory — dangling.
This is the classic std::vector gotcha:
std::vector<int> v = {1, 2, 3};
int* p = &v[0];
v.push_back(4); // may reallocate → p dangles
Rules to remember: don’t hold raw pointers/references/iterators across operations that may grow the vector; use indices (which survive reallocation) or re-fetch iterators after mutation, and use reserve() up front to avoid reallocation. The interview answer: reallocation on growth moves elements to a new buffer and frees the old one, invalidating outstanding pointers/references/iterators.
Answer:
Capacity growth reallocates the internal buffer, moving elements to new memory and freeing the old buffer — invalidating all existing pointers, references, and iterators.
When push_back/insert exceed the vector’s current capacity, the vector allocates a new, larger heap block, moves (or copies) the elements over, and frees the old block. Any pointer, reference, or iterator that pointed into the old block now points at freed memory — dangling.
This is the classic std::vector gotcha:
std::vector<int> v = {1, 2, 3};
int* p = &v[0];
v.push_back(4); // may reallocate → p dangles
Rules to remember: don’t hold raw pointers/references/iterators across operations that may grow the vector; use indices (which survive reallocation) or re-fetch iterators after mutation, and use reserve() up front to avoid reallocation. The interview answer: reallocation on growth moves elements to a new buffer and frees the old one, invalidating outstanding pointers/references/iterators.
Premium Content
Unlock Smart Pointers & Memory and all premium lessons with a subscription.
From ₹199.99/year — See plans