Experienced C++ Interview Questions

What is RAII in C++?

RAII (Resource Acquisition Is Initialization) ties the life of a resource (memory, file, lock, socket) to the life of an object. The constructor takes the resource, and the destructor releases it. When the object leaves its scope, C++ calls the destructor, even if an exception is thrown.

#include <iostream>
#include <mutex>
#include <stdexcept>

std::mutex m;

void risky() {
    std::lock_guard<std::mutex> lock(m);        // RAII: locks here
    std::cout << "locked\n";
    throw std::runtime_error("oops");           // the lock is still released
}

int main() {
    try {
        risky();
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << '\n';
    }
    std::cout << "can lock again: " << m.try_lock() << '\n';
}
Output

locked
caught: oops
can lock again: 1

Standard RAII types: std::unique_ptr, std::vector, std::string, std::lock_guard and std::ofstream. See Constructors and Destructors.

What is the Rule of Three, Five and Zero in C++?

RuleMeaning
Rule of ZeroDo not write special member functions. Let std::string, vector and smart pointers handle the resources
Rule of ThreeIf you need a custom destructor, copy constructor or copy assignment, you probably need all three
Rule of FiveAdd the move constructor and move assignment to the three
#include <iostream>
#include <string>
#include <vector>

struct Person {                                   // Rule of Zero: no special functions written
    std::string name;
    std::vector<int> scores;
};

int main() {
    Person a{"Riya", {80, 90}};
    Person b = a;                                  // a correct copy, made by the compiler
    Person c = std::move(a);                       // a correct move
    std::cout << b.name << ' ' << b.scores.size() << ' ' << c.name << ' ' << a.name.empty() << '\n';
}
Output

Riya 2 Riya 1

See Move Semantics and the Rule of Five.

What does std::move do in C++?

std::move(x) is only a cast to T&&. The real moving is done by the move constructor or move assignment. After a move, the old object is valid but its value is unspecified (usually empty).

#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string a(30, 'x');
    std::string b = a;                      // copy: both have the text
    std::string c = std::move(a);           // move: c takes the buffer
    std::cout << a.size() << ' ' << b.size() << ' ' << c.size() << '\n';
}
Output

0 30 30

Do not use std::move on a returned local variable: return x; already moves, and a manual std::move can stop the compiler from skipping the copy completely (copy elision). See Move Semantics.

What is the difference between unique_ptr, shared_ptr and weak_ptr?

TypeOwnersCopyCost
unique_ptrOneNo (move only)None
shared_ptrManyYesReference count (atomic)
weak_ptrNoneYesSmall
#include <iostream>
#include <memory>

int main() {
    auto u = std::make_unique<int>(1);
    auto v = std::move(u);                          // move the ownership

    auto s1 = std::make_shared<int>(2);
    std::weak_ptr<int> w = s1;                      // does not increase the count
    auto s2 = s1;
    std::cout << (u == nullptr) << ' ' << s1.use_count() << ' ' << w.use_count() << '\n';

    s1.reset();
    s2.reset();                                     // the last owner is gone
    std::cout << w.expired() << '\n';
    std::cout << (w.lock() == nullptr) << '\n';
}
Output

1 2 2
1
1

Start with unique_ptr. Use shared_ptr only when ownership is really shared. weak_ptr breaks cycles, where two shared objects point to each other. See Smart Pointers.

Why do we need a virtual destructor in C++?

If you delete a derived object through a base pointer and the base destructor is not virtual, only the base destructor runs. This is undefined behavior, and the resources of the derived class leak.

#include <iostream>
#include <memory>

struct Base {
    virtual ~Base() { std::cout << "~Base\n"; }
};

struct Derived : Base {
    ~Derived() override { std::cout << "~Derived\n"; }
};

int main() {
    std::unique_ptr<Base> p = std::make_unique<Derived>();
}   // both destructors run, because ~Base is virtual
Output

~Derived
~Base

Rule: a class which is used as a base class for polymorphism must have a virtual destructor (or a protected non virtual one). See Polymorphism.

How do virtual functions work in C++ (vtable and vptr)?

The compiler makes one vtable for each class which has virtual functions. It is a table of function pointers. Each object gets a hidden pointer, the vptr, to the vtable of its real class. A virtual call follows the vptr to find the right function at run time.

You can see the cost of the hidden pointer in the size of the objects:

#include <iostream>

struct Plain   { int x; };
struct Virtual { int x; virtual void f() {} };

int main() {
    std::cout << sizeof(Plain) << ' ' << sizeof(Virtual) << '\n';
}
Output

4 16

On a 64 bit system the virtual class is bigger by the size of a pointer (and padded). A virtual call costs a little more than a normal call, and the compiler can often not put it inline. See Virtual Functions.

What is object slicing in C++?

#include <iostream>

struct Base {
    virtual const char* who() const { return "Base"; }
};
struct Derived : Base {
    int extra = 42;
    const char* who() const override { return "Derived"; }
};

int main() {
    Derived d;
    Base sliced = d;                    // copies only the Base part
    const Base& ref = d;                // no copy
    std::cout << sliced.who() << ' ' << ref.who() << '\n';
}
Output

Base Derived

Pass polymorphic objects by reference, pointer or smart pointer, and store pointers in containers (std::vector<std::unique_ptr<Base>>), not objects. See Polymorphism.

What is const correctness in C++?

  • const variables: const int max = 10;
  • const reference parameters: read without a copy.
  • const methods: promise not to change the object (int size() const).
  • const pointers: const int* (value const) and int* const (pointer const).
#include <iostream>
#include <string>

class User {
public:
    explicit User(std::string n) : name(std::move(n)) {}
    const std::string& get_name() const { return name; }     // does not change the object
    void rename(std::string n) { name = std::move(n); }       // changes it: not const
private:
    std::string name;
};

void print(const User& u) {          // can only call const methods
    std::cout << u.get_name() << '\n';
    // u.rename("x");                // error if enabled
}

int main() {
    User u("Riya");
    print(u);
}
Output

Riya

Also the keyword mutable lets a member change inside a const method (used for caches and mutexes). See Classes.

What is undefined behavior in C++? Give examples.

Undefined behavior (UB) is code for which the standard gives no rules. The compiler may assume it never happens, so the result is anything: a crash, a wrong answer, or “it works on my machine”.

  • Reading a variable which is not initialized.
  • An array index out of range, or dereferencing nullptr.
  • Signed integer overflow (unsigned overflow is defined: it wraps).
  • Using an object after it is deleted (dangling pointer).
  • Two threads changing the same data without a lock (data race).
  • Modifying a const object, or a missing return in a function which returns a value.

Tools find UB while testing. We compile this code with the undefined behavior sanitizer:

#include <climits>
#include <iostream>

int main() {
    int x = INT_MAX;
    x = x + 1;                          // signed overflow: undefined behavior
    std::cout << x << '\n';
}
Terminal

$ g++ -std=c++23 -fsanitize=undefined ub.cpp -o ub
$ ./ub 2>&1 | sed 's/^[^ ]*ub.cpp/ub.cpp/'
ub.cpp:6:7: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
-2147483648

Use -fsanitize=address,undefined and -Wall -Wextra in every test build.

What is the difference between std::map and std::unordered_map?

Pointstd::mapstd::unordered_map
InsideBalanced treeHash table
OrderSorted by keyNo order
Find, insertO(log n)O(1) on average
Key needsoperator<hash and ==
IteratorsStay valid after insertCan be invalidated by a rehash
#include <iostream>
#include <map>
#include <string>
#include <unordered_map>

int main() {
    std::map<std::string, int> sorted{{"pear", 3}, {"apple", 1}, {"mango", 2}};
    for (const auto& [k, v] : sorted) std::cout << k << ' ';        // always sorted
    std::cout << '\n';

    std::unordered_map<std::string, int> fast{{"pear", 3}, {"apple", 1}};
    std::cout << fast.at("pear") << ' ' << fast.contains("kiwi") << '\n';
}
Output

apple mango pear
3 0

Choose unordered_map when you only look up by key. Choose map when you need the order or range searches. See STL Containers.

What is the difference between push_back and emplace_back in C++?

#include <iostream>
#include <string>
#include <vector>

struct Item {
    Item(std::string n, int q) : name(std::move(n)), qty(q) { std::cout << "construct " << name << '\n'; }
    Item(const Item& o) : name(o.name), qty(o.qty) { std::cout << "copy " << name << '\n'; }
    Item(Item&& o) noexcept : name(std::move(o.name)), qty(o.qty) { std::cout << "move " << name << '\n'; }
    std::string name;
    int qty;
};

int main() {
    std::vector<Item> v;
    v.reserve(4);                       // avoid moves caused by growing
    v.push_back(Item("pen", 1));        // builds a temporary, then moves it
    v.emplace_back("book", 2);          // builds directly in the vector
}
Output

construct pen
move pen
construct book

emplace_back avoids one move. For simple types the difference is small, and push_back is fine. See Vectors.

What is iterator invalidation in C++?

A change to a container can make old iterators, pointers and references invalid. Using them is undefined behavior. For a vector, push_back can move all the items to a bigger memory block when the capacity is full.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};
    const int* before = v.data();
    v.reserve(1000);                          // the data moves to a new place
    const int* after = v.data();
    std::cout << "data moved: " << (before != after) << '\n';
    // an iterator or pointer taken before reserve() must not be used now
}
Output

data moved: 1

ContainerAfter insertAfter erase
vectorAll invalid if it reallocates, else from the insert pointFrom the erased item onward
list, map, setStay validOnly the erased item is invalid
unordered_mapCan all become invalid on a rehashOnly the erased item

Safe habits: use indexes, call reserve early, and do not keep iterators across changes. See Vectors.

What are concepts in C++20?

A concept is a compile time condition on a type. You use it to say what a template needs.

#include <concepts>
#include <iostream>
#include <string>

template <typename T>
concept Printable = requires(T x) { std::cout << x; };

template <Printable T>
void show(const T& x) { std::cout << x << '\n'; }

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

int main() {
    show(42);
    show(std::string("text"));
    std::cout << half(9) << '\n';
}
Output

42
text
4

#include <concepts>

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

int main() {
    half(3.5);
}
Output

Wrong.cpp: In function 'int main()':
Wrong.cpp:7:9: error: no matching function for call to 'half(double)'
7 | half(3.5);
| ~~~~^~~~~
• there is 1 candidate
• candidate 1: 'template<class T> requires integral<T> T half(T)'
Wrong.cpp:4:3:
4 | T half(T x) { return x / 2; }
| ^~~~
• template argument deduction/substitution failed:
• constraints not satisfied
In file included from Wrong.cpp:1:
• /usr/local/include/c++/16.2.0/concepts: In substitution of 'template<class T> requires integral<T> T half(T) [with T = double]':
• required from here
Wrong.cpp:7:9:
7 | half(3.5);
| ~~~~^~~~~
• required for the satisfaction of 'integral<T>' [with T = double]
/usr/local/include/c++/16.2.0/concepts:109:13:
109 | concept integral = is_integral_v<_Tp>;
| ^~~~~~~~
• the expression 'is_integral_v<_Tp> [with _Tp = double]' evaluated to 'false'
/usr/local/include/c++/16.2.0/concepts:109:24:
109 | concept integral = is_integral_v<_Tp>;
| ^~~~~~~~~~~~~~~~~~

The error says clearly that double does not satisfy std::integral, instead of 50 lines of template messages. See Templates.

What is the difference between templates and macros in C++?

PointTemplateMacro
Done byCompilerPreprocessor (text)
Type safeYesNo
Scope and namespacesYesNo
DebuggingNormalHard
Side effectsNoneArguments can be evaluated twice
#include <iostream>

#define SQUARE(x) x * x

template <typename T>
constexpr T square(T x) { return x * x; }

int main() {
    int n = 3;
    std::cout << SQUARE(n + 1) << '\n';          // becomes n + 1 * n + 1 = 7: a macro bug
    std::cout << square(n + 1) << '\n';          // 16: correct
}
Output

7
16

Use constexpr, const, templates and inline functions instead of macros. Macros stay useful for include guards and conditional compiling. See Templates.

When to pass by value, reference or pointer in C++?

CaseUse
Small type (int, double, pointer)T by value
Big object, only readconst T&
Function must change the argumentT&
Argument may be missingT* (or std::optional)
Function keeps its own copyT by value, then std::move
Read only textstd::string_view
#include <iostream>
#include <string>
#include <string_view>
#include <vector>

void a(int n) { std::cout << "value " << n << '\n'; }
void b(const std::vector<int>& v) { std::cout << "const ref " << v.size() << '\n'; }
void c(int& n) { n++; }
void d(const int* p) { std::cout << (p ? "pointer set" : "no value") << '\n'; }
void e(std::string_view s) { std::cout << "view " << s << '\n'; }

int main() {
    int n = 1;
    a(n);
    b({1, 2, 3});
    c(n);
    d(&n);
    d(nullptr);
    e("text");
    std::cout << n << '\n';
}
Output

value 1
const ref 3
pointer set
no value
view text
2

See References and Pointers.

What is the explicit keyword in C++?

#include <iostream>

struct Meters {
    explicit Meters(double v) : value(v) {}
    double value;
};

void show(Meters m) { std::cout << m.value << '\n'; }

int main() {
    show(Meters(5));        // fine
    show(5);                // error: the hidden conversion is not allowed
}
Output

Implicit.cpp: In function 'int main()':
Implicit.cpp:12:10: error: could not convert '5' from 'int' to 'Meters'
12 | show(5); // error: the hidden conversion is not allowed
| ^
| |
| int
Implicit.cpp:4:14: note: explicit conversion function was not considered
4 | explicit Meters(double v) : value(v) {}
| ^~~~~~

Without explicit, show(5) would compile and quietly create a Meters. Mark single argument constructors explicit unless you really want the conversion. See Constructors.

What is perfect forwarding in C++ (std::forward)?

A function template can take any argument and pass it on without extra copies. T&& in a template is a forwarding reference, and std::forward<T>(x) keeps the original category.

#include <iostream>
#include <string>
#include <utility>

void take(const std::string&) { std::cout << "got an lvalue (copy needed)\n"; }
void take(std::string&&)      { std::cout << "got an rvalue (can move)\n"; }

template <typename T>
void forward_it(T&& x) {
    take(std::forward<T>(x));
}

int main() {
    std::string s = "text";
    forward_it(s);                      // lvalue
    forward_it(std::string("temp"));    // rvalue
}
Output

got an lvalue (copy needed)
got an rvalue (can move)

This is how std::make_unique, emplace_back and std::thread pass your arguments to constructors.

How do lambda captures work in C++?

CaptureMeaning
[x]x by value
[&x]x by reference
[=]All used variables by value
[&]All used variables by reference
[this]The current object pointer
[x = std::move(y)]Init capture (C++14): create a member
#include <iostream>
#include <memory>

int main() {
    int a = 1, b = 2;
    auto f = [a, &b] { b += a; return b; };
    std::cout << f() << ' ' << b << '\n';

    auto p = std::make_unique<int>(7);
    auto g = [q = std::move(p)] { return *q; };        // move into the lambda
    std::cout << g() << ' ' << (p == nullptr) << '\n';
}
Output

3 3
7 1

A lambda which is saved or returned must not capture local variables by reference, because the variables are destroyed. See Lambda Expressions.

What is new in C++20?

  • Concepts: named rules for templates.
  • Ranges and views: pipelines like v | views::filter(...) | views::transform(...).
  • Modules: a modern replacement for header files.
  • Coroutines: functions which can pause and continue.
  • std::format: type safe formatting.
  • Spaceship operator <=>: all comparisons with one function.
  • std::span, std::jthread, designated initializers and more.
#include <format>
#include <iostream>
#include <ranges>
#include <span>
#include <vector>

struct Point { int x; int y; };

int main() {
    Point p{.x = 1, .y = 2};                                              // designated initializers
    std::vector<int> v = {1, 2, 3, 4, 5, 6};
    auto evens = v | std::views::filter([](int n) { return n % 2 == 0; });
    std::span<const int> view{v};
    std::cout << std::format("{} {} {} {}\n", p.x + p.y, *evens.begin(), view.size(), (1 <=> 2) < 0);
}
Output

3 2 6 true

See Modern C++ Features.

What is new in C++23?

  • std::print and std::println: easy formatted output.
  • std::expected: a value or an error, without exceptions.
  • Deducing this: one member function for const and non const objects.
  • std::mdspan: a view of multidimensional data.
  • std::flat_map, std::flat_set, std::generator and more ranges features.
  • if consteval, std::unreachable, std::to_underlying and others.
#include <expected>
#include <print>
#include <utility>

enum class Color { Red = 1, Green = 2 };

std::expected<int, std::string> half(int n) {
    if (n % 2) return std::unexpected("odd number");
    return n / 2;
}

int main() {
    std::println("{}", half(10).value());
    std::println("{}", half(7).error());
    std::println("{}", std::to_underlying(Color::Green));
}
Output

5
odd number
2

All of these compile with GCC 16 and -std=c++23. See Modern C++ Features.