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';
}
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++?
| Rule | Meaning |
|---|---|
| Rule of Zero | Do not write special member functions. Let std::string, vector and smart pointers handle the resources |
| Rule of Three | If you need a custom destructor, copy constructor or copy assignment, you probably need all three |
| Rule of Five | Add 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';
}
Riya 2 Riya 1
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';
}
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?
| Type | Owners | Copy | Cost |
|---|---|---|---|
| unique_ptr | One | No (move only) | None |
| shared_ptr | Many | Yes | Reference count (atomic) |
| weak_ptr | None | Yes | Small |
#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';
}
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
~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';
}
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';
}
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) andint* 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);
}
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
constobject, 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';
}
$ 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?
| Point | std::map | std::unordered_map |
|---|---|---|
| Inside | Balanced tree | Hash table |
| Order | Sorted by key | No order |
| Find, insert | O(log n) | O(1) on average |
| Key needs | operator< | hash and == |
| Iterators | Stay valid after insert | Can 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';
}
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
}
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
}
data moved: 1
| Container | After insert | After erase |
|---|---|---|
| vector | All invalid if it reallocates, else from the insert point | From the erased item onward |
| list, map, set | Stay valid | Only the erased item is invalid |
| unordered_map | Can all become invalid on a rehash | Only 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';
}
42
text
4
#include <concepts>
template <std::integral T>
T half(T x) { return x / 2; }
int main() {
half(3.5);
}
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++?
| Point | Template | Macro |
|---|---|---|
| Done by | Compiler | Preprocessor (text) |
| Type safe | Yes | No |
| Scope and namespaces | Yes | No |
| Debugging | Normal | Hard |
| Side effects | None | Arguments 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
}
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++?
| Case | Use |
|---|---|
| Small type (int, double, pointer) | T by value |
| Big object, only read | const T& |
| Function must change the argument | T& |
| Argument may be missing | T* (or std::optional) |
| Function keeps its own copy | T by value, then std::move |
| Read only text | std::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';
}
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
}
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
}
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++?
| Capture | Meaning |
|---|---|
| [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';
}
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);
}
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_underlyingand 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));
}
5
odd number
2
All of these compile with GCC 16 and -std=c++23. See Modern C++ Features.