Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Top 25 - Part 1
C++

Top 25 - Part 1

Practice the first 15 questions from a curated set of the top 25 C++ programming interview questions.

1. 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 behaviordelete would try to free memory it doesn’t own. The correct teardown is the reverse of construction:

  1. Call the destructor manually: ptr->~T(); — this runs cleanup of any resources the object acquired (like a normal destructor would).
  2. 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.

2. Which std::memory_order provides the strongest synchronization guarantee in atomic operations?

Answer: std::memory_order_seq_cst (sequentially consistent).

The memory order argument to atomic operations controls how much reordering the compiler and CPU may perform around them. From weakest to strongest:

  • memory_order_relaxed — no ordering guarantees beyond the atomicity of the operation itself.
  • memory_order_acquire / release — one-way ordering for producer-consumer patterns.
  • memory_order_seq_cst — the default and the strongest: all operations tagged sequentially consistent appear in a single globally agreed-upon total order across every thread.

With seq_cst, if thread A observes a value stored before some operation by thread B, then every thread agrees on the ordering — there’s one consistent view of all the seq_cst operations. This is the easiest to reason about (matches intuitive “happens-before” thinking) at the cost of more synchronization on the hardware.

It’s also the default: std::atomic<T>::load()/store() use seq_cst unless you specify otherwise. The interview answer: std::memory_order_seq_cst — one total order of all atomic operations, preventing reordering across the fence.

3. What safety risk occurs when a lambda captures a local variable by reference ([&var])?

Answer: If the lambda outlives the variable’s scope, invoking it later reads a dangling reference — undefined behavior.

Capturing by reference does not extend the variable’s lifetime. It stores a reference to the variable. As long as the lambda is used within the variable’s lifetime, all is well. But the moment the lambda escapes that scope — returned from the function, stored in a container that outlives it, dispatched to an async thread — the reference dangles: it points at memory that’s already been destroyed.

auto makeLambda() {
    int x = 42;
    return [&x] { return x; };  // dangling when called later
}

The fix options:

  • Capture by value ([x]) — the closure owns a copy, safe to escape.
  • Capture by reference only when you’re certain the lambda is used strictly inside the variable’s scope.

This is the same lifetime hazard as a raw reference or pointer to a local — lambdas just make it easy to write by accident, because the escape is often indirect (passing a lambda to a thread or a callback). The interview answer: [&var] captures a reference that dangles if the lambda outlives var’s scope — UB on use.

4. How does dynamic_cast signal failure when attempting an invalid downcast on pointer versus reference types?

Answer: A pointer cast returns nullptr; a reference cast throws std::bad_cast.

dynamic_cast performs a checked, runtime polymorphic cast (requires a polymorphic base — at least one virtual function). When the cast fails, the failure reporting depends on the target kind:

  • Pointer: dynamic_cast<T*>(p) returns nullptr. Callers check for null.
  • Reference: dynamic_cast<T&>(r) cannot return “nothing” — there’s no null reference in valid C++. So it throws std::bad_cast.

The asymmetry exists because references must always be valid; a failed reference cast has nowhere to put a “no result,” so it must raise an exception.

Base* b = new Derived;
if (Derived* d = dynamic_cast<Derived*>(b)) { /* ok */ }

Base& br = *b;
Derived& dr = dynamic_cast<Derived&>(br);  // throws std::bad_cast if fails

The interview answer: pointer → nullptr; reference → std::bad_cast exception.

5. What design pattern or problem do C++20 Concepts simplify compared to C++11/14 SFINAE?

Answer: Template parameter constraints — replacing the verbose, cryptic SFINAE (std::enable_if) idiom with clear, readable, and better-diagnosing syntax.

Before C++20, constraining templates meant SFINAE (Substitution Failure Is Not An Error): buried typename std::enable_if<...>::type in template parameter lists, return types, or helper structs. It works, but it’s hard to read, produces horrific error messages when a constraint fails, and is a pain to compose.

C++20 concepts make constraints first-class:

template<typename T>
requires std::integral<T>   // a constraint
T half(T x) { return x / 2; }

or the compact form: template<std::integral T>. The benefits:

  • Readability — the intent (“T must be an integral type”) is stated directly.
  • Better diagnostics — a failed constraint check produces a message naming the concept, instead of a wall of substitution noise.
  • Overload resolution — concepts participate cleanly, and requires clauses can express relationships between parameters.

The interview answer: Concepts replace SFINAE/enable_if for constraining templates, with cleaner syntax, faster compile times, and far better error messages.

6. What will be the output of the following code regarding std::unique_ptr move mechanics?

std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::move(p1);
if (!p1) {
    std::cout << "p1 is null, ";
}
std::cout << *p2;

Output: p1 is null, 42.

std::unique_ptr is a move-only type — it owns its resource exclusively and cannot be copied. Moving it (std::move(p1)) transfers ownership to the target:

  • p2 now owns the int with value 42.
  • p1 is left in the valid-but-empty state: it holds nullptr.

So the check !p1 is true (it’s null), printing p1 is null, . Then *p2 safely dereferences the owned integer, printing 42. Output: p1 is null, 42.

The key behavior: moving a unique_ptr never copies the pointee; it just hands the raw pointer over and nulls the source. The interview answer: p1 is null, 42.

7. What is the fundamental issue with returning a reference to a local variable from a function?

Answer: The local variable is destroyed when the function’s stack frame pops, so the caller holds a dangling reference — dereferencing it is undefined behavior.

int& bad() {
    int x = 42;
    return x;   // x dies here
}

Local variables have automatic storage duration — they’re destroyed at the end of the scope where they’re declared. When bad() returns, x no longer exists, but the caller received a reference to where it used to be. Accessing that reference later reads freed stack memory: a dangling reference, which is UB.

(Even reading it once often “appears” to work because the stack memory hasn’t been overwritten — which makes the bug dangerous: it works in tests and corrupts mysteriously in production.)

The fixes:

  • Return by value — the value is copied/moved out (and thanks to copy elision, often no copy happens at all).
  • If you must return a reference, return one to an object with longer lifetime — a static, a class member, or a caller-supplied object.

The interview answer: returning a reference to a local yields a dangling reference — UB on access, because the local is destroyed when the frame exits.

8. What happens when an exception is thrown from a class destructor during an active stack unwinding phase caused by another exception?

Answer: std::terminate() is called immediately.

C++ cannot handle two exceptions simultaneously in flight. When an exception is being unwound (the first exception is propagating, destructors are running), if a destructor during that unwinding throws an exception of its own, the program can’t continue unwinding — there’s no mechanism to track both. The runtime calls std::terminate(), which by default aborts the program.

This is why destructors must not throw, and why modern C++ makes destructors noexcept by default: a destructor that throws while unwinding is a guaranteed terminate.

The engineering takeaway: destructors should be written to swallow or handle errors internally (log, clean up, but never propagate). The interview answer: the runtime immediately calls std::terminate() — two concurrent exceptions aren’t supported.

9. What is the size of an empty class struct in C++?

Answer: At least 1 byte (typically exactly 1).

An empty class has no data members, yet its objects must have a non-zero size. The reason is object identity: if empty objects were size 0, then two objects in an array could occupy the same address, and &a == &b would be true for distinct objects — which the standard forbids. Every object must have a distinct address.

So the compiler gives empty classes a minimum size of 1 byte. In arrays, each element gets its own byte, preserving distinct addresses.

A related optimization: the Empty Base Optimization (EBO) — an empty base class can take up no additional space when combined into a derived class. But a standalone empty object is at least 1 byte. The interview answer: non-zero, at least 1 byte, so distinct objects have distinct addresses.

10. What is the difference between std::thread and C++20 std::jthread?

Answer: std::jthread automatically joins on destruction and supports cooperative cancellation via std::stop_token.

The pain point with std::thread is RAII-discipline: a std::thread that goes out of scope while still joinable (not joined, not detached) calls std::terminate(). You must remember to join() or detach() manually — easy to get wrong in the presence of exceptions or early returns.

std::jthread fixes the lifetime problem: its destructor automatically requests cancellation and joins the thread, so you can’t forget. It’s safe under exceptions because destruction happens during unwinding.

It also adds cooperative cancellation: each jthread has an associated std::stop_token. Long-running work can check token.stop_requested() and exit early; the thread’s destructor can request a stop, giving a clean shutdown path instead of just killing the thread.

The interview answer: jthread auto-joins on destruction (RAII) and supports std::stop_token-based cancellation, unlike std::thread.

11. Which keyword prevents implicit type conversions during single-argument constructor calls?

Answer: explicit.

A single-argument (or defaulted-with-args) constructor normally acts as an implicit conversion from its parameter type: passing an int where a String is expected would silently construct a String from the int. That implicit conversion is often surprising and sometimes dangerous (it can hide bugs or enable accidental conversions).

Marking the constructor explicit forbids implicit conversions:

  • String(int) — implicit conversion allowed: String s = 5; compiles.
  • explicit String(int) — only explicit construction: String s(5); or String s = String(5); compiles; String s = 5; is an error.

explicit applies to constructors and conversion operators, and it’s used to require callers to state their intent. The interview answer: explicit prevents implicit single-argument conversions, forcing explicit construction.

12. What happens if a function marked noexcept throws an unhandled exception at runtime?

Answer: std::terminate() is invoked immediately — without full stack unwinding.

noexcept is a promise: “this function will not propagate an exception.” If that promise is broken — an exception escapes the function — the runtime calls std::terminate() directly. terminate is the ultimate handler; by default it aborts the program.

The critical detail: because the compiler and runtime treat noexcept as a hard contract, stack unwinding is not performed. Destructors of local objects in the noexcept function do not necessarily run. This is why noexcept must not be applied lightly to functions that can throw — it turns a recoverable error into a hard abort and skips cleanup.

The interview answer: an escaping exception from a noexcept function calls std::terminate() immediately, without unwinding.

13. What is the underlying cause of “False Sharing” in concurrent C++ programs?

Answer: Independent variables used by different threads happen to sit on the same CPU cache line, causing cache-line invalidation thrashing.

CPU caches are organized in lines (typically 64 bytes). When a core writes to memory, it must own the cache line — which means invalidating that line on all other cores. Now suppose two independent variables a and b (accessed only by thread 1 and thread 2 respectively) land on the same cache line. There’s no data race — the threads touch different variables. But:

  • Thread 1 writes a → invalidates the line on thread 2’s core.
  • Thread 2 writes b → invalidates the line on thread 1’s core.

Every write forces a cache miss on the other core, even though neither thread touched the other’s data. Performance collapses, despite correctness being fine. That’s “false” sharing — the sharing is an artifact of cache-line granularity, not of actual data sharing.

The fix: padding — align or space the variables so they occupy separate cache lines (alignas(64), or pad the struct to a multiple of 64 bytes). The interview answer: distinct variables sharing a cache line cause invalidation thrashing between cores — no logical race, but a severe performance problem.

14. What is the key functional difference between reinterpret_cast and static_cast?

Answer: static_cast performs conversions the compiler can check at compile time based on known type relationships; reinterpret_cast reinterprets the raw bit pattern between unrelated types without any checks.

static_cast operates within the rules of the type system: numeric conversions (intdouble), up/down casts within a known inheritance hierarchy, void* to typed pointer, and so on. The compiler validates the relationship — a downcast through static_cast assumes the object really is that derived type (no runtime check), but the types must be related.

reinterpret_cast ignores the type system entirely: it tells the compiler “just treat this bit pattern as that other type.” reinterpret_cast<int*>(&someFloat) reinterprets the bytes of a float as an int pointer — no relationship required, no check performed. This is exactly the territory where strict-aliasing UB lives.

The contrast:

  • static_cast — safe-ish, compile-time-checked, type-system-aware.
  • reinterpret_cast — raw bit reinterpretation, unchecked, easily UB.

The interview answer: static_cast uses compile-time-known relationships; reinterpret_cast blindly reinterprets bit patterns with no verification.

15. What causes undefined behavior in this string initialization statement?

const char* ptr = "Hello, World!";
char* str = const_cast<char*>(ptr);
str[0] = 'h';

Answer: String literals may live in read-only memory; modifying them — even after const_cast removes const — is undefined behavior.

String literals are stored in read-only memory (in typical implementations). The const char* declaration is not just a type qualifier — it reflects that the object genuinely cannot be modified.

const_cast removes the const qualifier from the pointer type, so char* str = const_cast<char*>(ptr) compiles and str[0] = 'h' is legal syntax. But it doesn’t change where the literal lives. Writing through str attempts to modify read-only memory: a crash (access violation) in practice, and undefined behavior per the standard.

The rules to remember:

  • const_cast is for removing const from objects you know are actually mutable (e.g., a const reference to a non-const variable).
  • Casting away const from a truly const object and modifying it is UB.

The interview answer: writing to a string literal through a const_cast-stripped pointer is UB — the literal is in read-only memory.

My Private Notes

Notes are auto-saved locally to this device.