Skip to content
C++
Idiom
since C++11
Advanced

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.

cpp
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?"

cpp
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 guarantee

The 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."

cpp
// 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."

cpp
// 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?"

cpp
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:

cpp
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() correctly

override prevents silent bugs:

cpp
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?"

cpp
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:

cpp
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 const

Common Gotchas Interviewers Love

Most Vexing Parse

cpp
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

cpp
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)

cpp
// 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

cpp
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 downcasts

Quick Reference: Questions and One-Line Answers

QuestionKey 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.

cpp
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.

cpp
// 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.

cpp
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 devirtualized

dynamic_cast and RTTI:

cpp
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 Dog

When 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.

cpp
// 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

QuestionKey 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 violationAccessing 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