C++ Interview Prep: Core Language Concepts
Master the C++ core language questions that come up in senior interviews: value categories, move semantics, RAII, const-correctness, virtual dispatch, and common gotchas.
This page covers the questions interviewers use to separate candidates who know C++ syntax from those who understand why C++ works the way it does. Each question includes the ideal answer and the traps candidates fall into.
Value Categories (lvalue, rvalue, xvalue)
The question: "What is the difference between lvalue, rvalue, prvalue, and xvalue?"
What it tests: Whether you understand the object model, not just how && looks.
int x = 42; // x is an lvalue β has identity (address), lives beyond expression
int* p = &x; // OK: x is addressable
42 + 1; // prvalue β pure temporary, no identity
int&& r = 42; // prvalue materializes into xvalue when bound to r
std::string s = "hello";
std::string&& rv = std::move(s); // std::move casts s to xvalue (expiring lvalue)
// s is still "alive" β it's just been told it can be moved from
// Key rule: a NAMED rvalue reference IS an lvalue
void f(std::string&& s) {
// s is an lvalue here! It has a name.
g(s); // calls g(const string&) β copies
g(std::move(s)); // explicitly cast to xvalue β moves
}The trap: Saying "an rvalue reference is always an rvalue." A named && variable is an lvalue β only unnamed temporaries and std::move() results are xvalues.
Move Semantics β noexcept Matters
The question: "Why should move constructors be noexcept?"
class Buffer {
std::byte* data_;
std::size_t size_;
public:
Buffer(Buffer&& other) noexcept // without noexcept: vector copies instead of moving!
: data_(other.data_), size_(other.size_)
{
other.data_ = nullptr;
other.size_ = 0;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
};
// Why it matters:
std::vector<Buffer> v;
v.push_back(Buffer{...}); // if push_back triggers reallocation and
// move constructor is NOT noexcept,
// vector falls back to copy for strong exception guaranteeThe trap: Omitting noexcept. std::vector uses std::move_if_noexcept during reallocation β if your move constructor is not noexcept, the vector copies every element, defeating the purpose of move semantics.
Rule of Five / Rule of Zero
The question: "Explain the Rule of Zero and the Rule of Five."
// Rule of Zero β preferred: let the compiler handle everything
class Config {
std::string path_; // std::string handles its own copying/moving
std::vector<int> data_; // std::vector handles its own copying/moving
// No destructor, no copy/move constructors needed β they're all generated correctly
};
// Rule of Five β required when you manage raw resources
class RawBuffer {
char* ptr_;
std::size_t size_;
public:
~RawBuffer() { delete[] ptr_; }
RawBuffer(const RawBuffer& o) : ptr_(new char[o.size_]), size_(o.size_)
{ std::copy(o.ptr_, o.ptr_ + size_, ptr_); }
RawBuffer& operator=(const RawBuffer& o) { RawBuffer tmp{o}; swap(tmp); return *this; }
RawBuffer(RawBuffer&& o) noexcept : ptr_(o.ptr_), size_(o.size_)
{ o.ptr_ = nullptr; o.size_ = 0; }
RawBuffer& operator=(RawBuffer&& o) noexcept {
swap(o); // give o our old data (it will destroy it)
return *this;
}
void swap(RawBuffer& o) noexcept {
std::swap(ptr_, o.ptr_);
std::swap(size_, o.size_);
}
};Key insight: If you define any of the five β destructor, copy constructor, copy assignment, move constructor, move assignment β you should define all five. A user-declared destructor suppresses implicit generation of the move constructor and move assignment β your class silently falls back to copy-everywhere.
RAII β The Correct Definition
The question: "What is RAII? Give me a real-world example beyond unique_ptr."
// RAII = tie a resource's lifetime to an object's lifetime
// Acquisition in constructor, release in destructor, guaranteed by stack unwinding
// Example: database transaction
class Transaction {
Database& db_;
bool committed_ = false;
public:
explicit Transaction(Database& db) : db_(db) { db_.begin(); }
void commit() { db_.commit(); committed_ = true; }
~Transaction() noexcept {
if (!committed_) { // rollback if commit was never called
try { db_.rollback(); }
catch (...) {} // destructors must never throw
}
}
Transaction(const Transaction&) = delete;
Transaction& operator=(const Transaction&) = delete;
};
void transfer(Database& db, int from, int to, int amount) {
Transaction txn{db};
db.debit(from, amount);
db.credit(to, amount); // if this throws, ~Transaction() rolls back automatically
txn.commit();
}Why it matters: RAII is why C++ code is exception-safe without try/finally. The destructor is called during stack unwinding whether the function returned normally or threw. This is the fundamental advantage over languages that require finally blocks.
virtual β vtable, override, and slicing
The question: "Why must base class destructors be virtual?"
struct Base {
virtual void f() {}
// NO virtual destructor β this is wrong for polymorphic base classes!
};
struct Derived : Base {
std::string data{"expensive string"};
~Derived() { /* clean up data */ }
};
Base* p = new Derived;
delete p; // UB: calls Base::~Base() only, Derived::~Derived() never called
// data is leaked; undefined behavior
// Fix:
struct CorrectBase {
virtual void f() {}
virtual ~CorrectBase() = default; // virtual destructor
};The slicing problem:
void print(Base b) { // takes by VALUE β slices!
b.f(); // calls Base::f() even if passed a Derived
}
Derived d;
print(d); // d is sliced to Base β Derived portion discarded
// Fix: always use pointer or reference for polymorphism
void print(const Base& b) { b.f(); } // calls Derived::f() correctlyoverride prevents silent bugs:
struct Base {
virtual void process(int x) {}
};
struct Wrong : Base {
void process(long x) {} // silently hides, doesn't override (different signature)
};
struct Correct : Base {
void process(int x) override {} // compiler error if signature doesn't match
};const-Correctness
The question: "What is mutable and when is it legitimate to use it?"
class Cache {
mutable std::unordered_map<int, Result> cache_; // mutable: modified in const method
mutable std::mutex mutex_; // mutable: locked in const method
public:
// Conceptually const β doesn't change the observable state of the object
Result get(int key) const {
std::lock_guard lock{mutex_};
auto it = cache_.find(key);
if (it != cache_.end()) return it->second;
auto result = expensive_compute(key);
cache_[key] = result; // OK: cache_ is mutable
return result;
}
};Legitimate mutable uses: lazily-computed caches, mutexes in thread-safe const methods, reference counts. Never use mutable to bypass const-correctness on observable state.
const pointer vs pointer to const:
int x = 10, y = 20;
const int* p1 = &x; // pointer to const int: can't modify *p1, can change p1
p1 = &y; // OK
// *p1 = 30; // error
int* const p2 = &x; // const pointer to int: can modify *p2, can't change p2
*p2 = 30; // OK
// p2 = &y; // error
const int* const p3 = &x; // both constCommon Gotchas Interviewers Love
Most Vexing Parse
Widget w(Foo()); // NOT construction β parses as function declaration!
// Declares function w that returns Widget and takes Foo(*)(void)
Widget w{Foo{}}; // fix: uniform initialization
Widget w((Foo())); // fix: extra parens (ugly)Undefined Behavior on Signed Overflow
int x = INT_MAX;
x + 1; // UB! signed integer overflow is undefined in C++
// compiler may assume it never happens and optimize accordingly
unsigned u = UINT_MAX;
u + 1; // defined: wraps to 0 (unsigned overflow is well-defined)Order of Evaluation (before C++17)
// Pre-C++17: evaluation order of arguments is unspecified
f(++i, i); // UB: reads and modifies i in the same expression (before C++17)
// C++17 tightened some rules but not all
// Safe pattern: use separate statements
++i;
f(i, i);static_cast vs dynamic_cast
Base* b = new Derived;
auto* d1 = static_cast<Derived*>(b); // no runtime check β UB if b isn't Derived
auto* d2 = dynamic_cast<Derived*>(b); // runtime check β returns nullptr if fails
// dynamic_cast requires a virtual function in Base (for RTTI)
// static_cast preferred when you know the type; dynamic_cast for checked downcastsQuick Reference: Questions and One-Line Answers
| Question | Key answer |
|---|---|
Why noexcept on move? | vector uses copy instead of move if move can throw |
| What is an xvalue? | Named temporary: result of std::move() or cast to T&& |
| Rule of Zero? | Let RAII member types handle everything; define no special members |
| Why virtual destructor? | Without it, delete base_ptr leaks derived portion |
When is mutable OK? | Caches, mutexes β observable state unchanged |
| Slicing happens when? | Passing derived object by value to base parameter |
| Most Vexing Parse fix? | Use {} initialization instead of () |
static_cast vs dynamic_cast? | static_cast = compile-time; dynamic_cast = runtime, returns nullptr |
Value Categories in Depth: prvalue, xvalue, glvalue
Interviewers use this question to filter for candidates who truly understand the C++11 object model, not just && syntax. Every expression in C++ has both a type and a value category; confusing the two is a reliable senior-engineer stumbling block. The full taxonomy has five leaves β lvalue, prvalue, xvalue β which group into two supertypes: glvalue (lvalue or xvalue) and rvalue (prvalue or xvalue).
A prvalue (pure rvalue) is an expression that does not have identity: the literal 42, a function returning T by value, or the result of a + b. A glvalue (generalized lvalue) has identity β it refers to an existing object that has an address. An xvalue is the intersection: it has identity (a name or a pointer you could theoretically take) but is flagged as "expiring" so it can be moved from.
int x = 10; // x is an lvalue (glvalue): has identity, not expiring
int&& r = 42; // 42 is a prvalue: no identity; materializes into a temporary
std::string s = "hello";
std::string&& rv = std::move(s);
// std::move(s) returns string&&, which is an xvalue:
// β it still refers to a real object (has identity)
// β but signals "you may steal my resources"
// Materialization: a prvalue materializes into an xvalue when bound to a reference
// or when a member is accessed from a class prvalue.
const std::string& r2 = std::string("world"); // prvalue materializes; lifetime extended
// The named-rvalue trap:
void sink(std::string&& s) {
// s is an LVALUE here β it has a name!
use(s); // copies β you lost the move
use(std::move(s)); // explicit move β correct
}Why interviewers ask: This reveals whether you know that passing a named && parameter to another function will copy unless you forward or move it β a subtle performance bug that survives code review.
Undefined Behavior: the "What Does This Print?" Trap
Interviewers hand you a short snippet and ask "what does this print?" β but the real answer is "the program has undefined behavior, and the output is meaningless." This tests whether you recognize UB rather than reasoning about what a particular compiler happens to do. There are three classics: signed overflow, strict aliasing violations, and use-after-free / null dereference.
Signed integer overflow is the most surprising: because the C++ standard defines signed overflow as UB, compilers are allowed to assume it never happens. A loop that counts up to INT_MAX may be compiled into an infinite loop if the optimizer proves the increment can never reach the overflow point β and then removes the termination condition entirely.
// Classic 1: signed overflow
int i = INT_MAX;
if (i + 1 < 0) { // UB: signed overflow
std::cout << "negative\n";
}
// GCC with -O2 may eliminate the branch entirely (it "knows" overflow can't happen)
// The "safe" fix:
if (i < INT_MAX) { /* OK to increment */ }
// Classic 2: strict aliasing
float f = 1.0f;
int* p = reinterpret_cast<int*>(&f); // UB: accessing float through int*
std::cout << *p;
// Allowed alternative: memcpy or std::bit_cast<int>(f) (C++20)
int bits;
std::memcpy(&bits, &f, sizeof(bits)); // defined: copies bytes, no aliasing
// Classic 3: reading an indeterminate value
int x; // uninitialized local β indeterminate value
std::cout << x; // UB: use of indeterminate value (even though it "usually prints 0")
// Classic 4: dangling reference
int& bad_ref() {
int local = 42;
return local; // returning reference to stack variable β UB to use the return value
}
// Classic 5: null dereference
int* ptr = nullptr;
std::cout << *ptr; // UB β even though it "always segfaults in practice"The interviewer's point: Saying "it prints X" without mentioning UB signals that you reason about C++ as if it were assembly. The correct answer names the UB, explains what the standard says, and describes the safe alternative.
Virtual Dispatch Internals: vtable and dynamic_cast
Every polymorphic class in C++ has a vtable β a compiler-generated array of function pointers β and every object of that class carries a hidden pointer to it. This is how virtual dispatch achieves runtime polymorphism without the programmer writing a switch statement. Understanding the vtable layout is a reliable senior-interview differentiator because it explains both the cost of virtual calls and why certain patterns (devirtualization, final, sealing) can eliminate that cost.
When a virtual function is called through a pointer or reference, the compiler emits: load vptr from object, load function pointer from vtable at the known slot index, indirect call. That indirect branch is typically 1β5 cycles on a modern CPU when the branch predictor is warm, but can balloon to 10β50 cycles on a cold cache miss β relevant for tight loops over heterogeneous containers.
struct Animal {
virtual void speak() { std::cout << "...\n"; }
virtual ~Animal() = default;
// Hidden member (conceptually): Animal_vtable* vptr;
};
struct Dog : Animal {
void speak() override { std::cout << "Woof\n"; }
};
struct Cat : Animal {
void speak() override { std::cout << "Meow\n"; }
};
// At runtime:
Animal* a = new Dog;
a->speak(); // β load a->vptr, fetch vptr[speak_slot], call it β "Woof"
// Cost breakdown:
// 1. Dereference a (may miss cache β random heap pointer)
// 2. Load vptr (first 8 bytes of object β usually hot)
// 3. Load function ptr (vtable slot β often hot if few derived types)
// 4. Indirect call (branch predictor may succeed if the same type each time)
// Devirtualization: compiler can eliminate the overhead when it can prove the type
Dog d;
d.speak(); // compiler knows static type is Dog β direct call, zero virtual overhead
// final eliminates virtual overhead for further derivation:
struct FastDog final : Animal {
void speak() override { std::cout << "Woof\n"; }
};
// No class can derive from FastDog, so calls through FastDog* can be devirtualizeddynamic_cast and RTTI:
Animal* a = new Dog;
// dynamic_cast: runtime type check using RTTI stored alongside vtable
Dog* d = dynamic_cast<Dog*>(a); // returns non-null β a really is a Dog
Cat* c = dynamic_cast<Cat*>(a); // returns nullptr β a is not a Cat
// Requirements:
// β Base class must have at least one virtual function (enables RTTI)
// β Compile with RTTI enabled (default; -fno-rtti disables it)
// dynamic_cast for references: throws std::bad_cast instead of returning null
try {
Dog& dref = dynamic_cast<Dog&>(*a); // OK
Cat& cref = dynamic_cast<Cat&>(*a); // throws std::bad_cast
} catch (const std::bad_cast& e) { }
// Cost: O(depth of inheritance hierarchy), not O(1) β avoid in hot paths
// Alternative when hierarchy is known: static_cast (no check, zero cost)
Dog* d2 = static_cast<Dog*>(a); // UB if a is not actually a DogWhen Does the Compiler Generate Special Members?
The rules for compiler-generated special members are one of the most-tested topics in senior C++ interviews because the interactions are subtle and breaking them causes silent performance regressions. The compiler will generate a default constructor, copy constructor, copy assignment, move constructor, move assignment, and destructor β but user-declared versions of any of these suppress the automatic generation of others in ways that are not immediately obvious.
The most dangerous interaction: declaring a destructor suppresses implicit generation of the move constructor and move assignment operator. This means a class with a non-trivial destructor silently becomes copy-only unless you explicitly default the move operations. In a codebase that was written pre-C++11 (when there was no move), this is common β and causes vector reallocation to copy instead of move.
// Compiler generates all six if you declare nothing:
struct Simple {
int x;
// default ctor, copy ctor, copy=, move ctor, move=, destructor: all generated
};
// Declaring a destructor: suppresses move constructor and move assignment!
struct Logged {
~Logged() { std::cout << "destroyed\n"; }
// copy ctor and copy= are still generated (deprecated, but generated)
// move ctor and move= are NOT generated β falls back to copy everywhere
};
// Fix: explicitly default the move operations
struct LoggedFixed {
~LoggedFixed() { std::cout << "destroyed\n"; }
LoggedFixed(LoggedFixed&&) noexcept = default;
LoggedFixed& operator=(LoggedFixed&&) noexcept = default;
};
// Declaring any constructor suppresses the default constructor:
struct HasCustomCtor {
HasCustomCtor(int x) {}
// No default constructor!
// HasCustomCtor hcc; // ERROR: no default constructor
};
// The full suppression table (simplified):
// User declares β | default ctor | copy ctor | copy= | move ctor | move= | dtor
// nothing | gen | gen | gen | gen | gen | gen
// any constructor | NOT gen | gen | gen | gen | gen | gen
// copy ctor | NOT gen | user | gen | NOT gen | NOT gen | gen
// copy= | gen | gen | user | NOT gen | NOT gen | gen
// move ctor | NOT gen | =delete | =delete | user | NOT gen | gen
// move= | NOT gen | =delete | =delete | NOT gen | user | gen
// destructor | gen | gen | gen | NOT gen | NOT gen | user
// Practical advice: if you declare any of the five, declare all five (Rule of Five)
// or declare none and use RAII member types (Rule of Zero)
struct SafeResource {
std::unique_ptr<int[]> data; // Rule of Zero β unique_ptr handles everything
// No need to declare any special member
};The key interview answer: "Declaring a destructor alone is enough to disable move semantics. The safe pattern is either the Rule of Zero (delegate everything to RAII members) or the Rule of Five (declare all five explicitly)."
Quick Reference: Expanded
| Question | Key answer |
|---|---|
| What is a prvalue? | An expression with no identity β pure temporary; 42, a+b, function return by value |
| What is an xvalue? | An expression with identity that is flagged expiring: result of std::move() or T&& cast |
| What is a glvalue? | Supercategory: lvalue or xvalue β any expression that refers to an existing object |
Named && parameter inside a function β lvalue or rvalue? | Lvalue β it has a name; must std::move() to forward as rvalue |
| Signed overflow UB β what can compiler do? | Assume it never happens; may remove checks, unroll loops infinitely |
| Strict aliasing violation | Accessing object through pointer of unrelated type is UB; use memcpy or std::bit_cast |
| vtable slot determined when? | Compile time (known offset); called pointer loaded at runtime |
dynamic_cast cost? | O(hierarchy depth) RTTI walk; avoid in hot paths |
dynamic_cast on reference? | Throws std::bad_cast on failure (vs nullptr for pointer) |
| Declaring destructor suppresses what? | Move constructor and move assignment; class silently copies instead of moving |
| Declaring move constructor suppresses what? | Copy constructor and copy assignment become = delete |