C++
Modern C++ (C++11 and later) is a very different language from the C++ of the 1990s. Smart pointers, move semantics, lambdas, and constexpr transformed how idiomatic C++ is written. Raw new/delete is rare in modern codebases.
🟢 Junior
Types, References, and Const Correctness
C++ distinguishes values, references, and pointers at the type level.
int x = 42;
int& ref = x; // reference — alias for x, cannot be null, cannot be reseated
int* ptr = &x; // pointer — can be null, can point elsewhere
ref = 10; // modifies x
*ptr = 20; // modifies x via pointer dereference
ptr = nullptr; // pointer can now point to nothing
const int& cref = x; // read-only reference — common for function parameters
Pass large objects by const& to avoid copying. Pass small types (int, char, pointer) by value.
Classes and Access Control
class BankAccount {
public:
explicit BankAccount(double initial_balance)
: balance_(initial_balance) {} // member initializer list
void deposit(double amount) {
if (amount > 0) balance_ += amount;
}
bool withdraw(double amount) {
if (amount > 0 && balance_ >= amount) {
balance_ -= amount;
return true;
}
return false;
}
double balance() const { return balance_; } // const method — cannot modify members
private:
double balance_; // trailing underscore convention for private members
};
BankAccount acc(100.0);
acc.deposit(50.0);
acc.withdraw(30.0);
acc.balance(); // 120.0
explicit prevents implicit single-argument constructor calls — always use it to avoid accidental conversions.
std::vector and Common Containers
#include <vector>
#include <string>
#include <algorithm>
std::vector<int> nums = {5, 2, 8, 1, 9};
nums.push_back(3);
nums.emplace_back(7); // constructs in-place — avoids copy for complex types
nums.reserve(100); // pre-allocate — avoids reallocations in a known-size loop
std::sort(nums.begin(), nums.end());
auto it = std::find(nums.begin(), nums.end(), 8);
bool found = (it != nums.end());
Other key containers: std::map<K,V> (sorted tree), std::unordered_map<K,V> (hash table), std::set<T>, std::deque<T>, std::array<T,N> (fixed-size stack array).
🟡 Medior
RAII and Smart Pointers
RAII (Resource Acquisition Is Initialization) ties a resource’s lifetime to an object’s lifetime — the destructor releases the resource. Smart pointers implement RAII for heap allocations.
#include <memory>
// unique_ptr — sole ownership, move-only
std::unique_ptr<int> p = std::make_unique<int>(42);
*p = 100;
// p goes out of scope → memory automatically freed, no delete needed
// shared_ptr — shared ownership via reference counting
std::shared_ptr<std::string> s1 = std::make_shared<std::string>("hello");
std::shared_ptr<std::string> s2 = s1; // reference count = 2
s1.reset(); // count = 1, string still alive
// s2 goes out of scope → count = 0, string freed
// weak_ptr — non-owning observer, breaks reference cycles
std::weak_ptr<std::string> w = s1;
if (auto locked = w.lock()) {
// locked is a shared_ptr, valid only inside this block
}
Never use raw new/delete — use make_unique and make_shared instead.
Move Semantics and std::move
Copy is expensive for large objects. Move transfers ownership of resources (heap memory, file handles) from one object to another — it is O(1) regardless of size.
std::vector<int> source(1'000'000, 0);
std::vector<int> dest = std::move(source); // no copy — pointer swap
// source is now in a valid but unspecified state (do not use its contents)
When you write a class that manages a resource, implement the Rule of Five: destructor, copy constructor, copy assignment, move constructor, move assignment. Or use the Rule of Zero: delegate ownership to smart pointers and let the compiler generate everything.
Lambdas and std::function
Lambdas are anonymous function objects that can capture local variables.
int threshold = 5;
auto is_big = [threshold](int x) { return x > threshold; };
// [=] captures everything by value, [&] by reference, [x, &y] mixed
std::vector<int> nums = {1, 8, 3, 9, 2, 7};
nums.erase(
std::remove_if(nums.begin(), nums.end(), is_big),
nums.end()
); // nums is now [1, 3, 2]
std::function<R(Args...)> is a type-erased callable — slower than a plain lambda due to heap allocation and virtual dispatch. Use it only when you need to store heterogeneous callables (e.g., a std::vector<std::function<void()>>).
Templates
Templates generate code for specific types at compile time. There is no runtime overhead.
template<typename T>
T clamp(T value, T lo, T hi) {
return std::max(lo, std::min(value, hi));
}
clamp(15, 0, 10); // int version
clamp(3.7, 0.0, 5.0); // double version
// Class template
template<typename T, std::size_t N>
struct Stack {
std::array<T, N> data;
std::size_t top = 0;
void push(const T& v) { data[top++] = v; }
T pop() { return data[--top]; }
};
Stack<int, 100> stack;
stack.push(42);
C++20 Concepts constrain template parameters with readable error messages:
template<std::integral T>
T factorial(T n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
🔴 Senior
Memory Layout and Cache Effects
Modern CPUs are 100–1000× faster than RAM. Cache misses are the #1 performance bottleneck in C++ code.
Struct of Arrays (SoA) is more cache-friendly than Array of Structs (AoS) when iterating over one field of many objects:
// AoS — iterating over x forces loading y and z too
struct Particle { float x, y, z, mass; };
std::vector<Particle> particles;
// SoA — iterating over x only touches x memory
struct Particles {
std::vector<float> x, y, z, mass;
std::size_t count;
};
Game engines and physics simulations use SoA for this reason. Profile with perf (Linux) or Intel VTune before switching — the transformation adds code complexity.
Concurrency with std::thread and Atomics
#include <thread>
#include <atomic>
#include <mutex>
std::atomic<int> counter{0}; // lock-free for simple increments
void increment_many() {
for (int i = 0; i < 100'000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
std::thread t1(increment_many), t2(increment_many);
t1.join(); t2.join();
// counter == 200'000
// For complex critical sections, use mutex
std::mutex mu;
std::vector<int> shared_log;
void log_event(int event) {
std::lock_guard<std::mutex> lock(mu); // RAII — unlocks automatically
shared_log.push_back(event);
}
std::memory_order_relaxed allows the compiler and CPU to reorder the atomic operation — use only when ordering doesn’t matter (e.g., simple counters). Use memory_order_seq_cst (the default) when you need sequential consistency.
Undefined Behavior
UB means the C++ standard makes no guarantee about what the program does. In practice, the optimizer exploits UB to produce incorrect-but-fast code.
Signed integer overflow is UB in C++. The compiler assumes it cannot happen and optimizes accordingly:
// Compiler may optimize this loop assuming i never overflows (wraps around)
for (int i = 0; i >= 0; i++) { ... }
// With UB, compiler may turn this into an infinite loop
Out-of-bounds array access — arr[i] where i >= size is UB. Sanitize with bounds checks in debug builds (-D_GLIBCXX_DEBUG, AddressSanitizer -fsanitize=address).
Use-after-free and dangling references:
int* get_local() {
int x = 42;
return &x; // UB — x is destroyed when function returns
}
std::string_view sv;
{
std::string s = "hello";
sv = s; // sv points into s
}
// sv is now a dangling reference — UB to use it
Always use -fsanitize=address,undefined in debug/test builds to catch these at runtime.
Senior Gotchas
The most dangerous rule: initialize all members in constructors. Uninitialized members have indeterminate values — reading them is UB, not necessarily zero.
std::vector::push_back that causes a reallocation invalidates all iterators, pointers, and references into the vector. Storing an iterator, then appending, then using the iterator is a subtle bug.
Virtual destructor: if a class is used polymorphically (base class pointer to derived object), the base class destructor must be virtual. Without it, delete base_ptr only calls the base destructor — the derived destructor is skipped and resources leak.
const_cast to remove const and then write through the resulting pointer is UB if the original object was actually const. It is only safe to use const_cast to pass a non-const pointer to a legacy API that should have taken const but doesn’t.