C++ Coding Interview Gotchas: The Traps That Cost You the Offer

May 29, 202611 min read
dsaalgorithmsinterview-prepdata-structures
C++ Coding Interview Gotchas: The Traps That Cost You the Offer
TL;DR
  • Signed integer overflow is undefined behavior in C++, not wraparound; cast to long long before multiplying coordinates near 10^9
  • map[key] inserts a default value when the key is missing; use .find() or .count() to check membership safely
  • Iterator invalidation happens on any vector modification; capture the return value of erase() rather than advancing the iterator manually
  • std::remove does not resize the container; always pair it with .erase() or the elements remain in place
  • priority_queue<int> is a max-heap by default; pass greater<int> as the comparator for min-heap behavior
  • String concatenation with + in a loop is O(n²); use += or append() for amortized O(1)
  • vector<bool> returns proxy objects, not real references; use vector<char> when you need a mutable bool container

C++ gives you more control than any other language in a coding interview. It also gives you more ways to silently produce the wrong answer while looking completely confident. The insidious part: these aren't obscure corner cases. They show up in the first twenty minutes and produce output that looks plausible until you trace through it by hand and feel the slow horror creep in.

Nobody fails a C++ interview because they forgot how a hash map works. They fail because they typed map[key] when they meant to check if the key exists, and now their map has a phantom entry with value zero, and the interviewer is writing something in their notebook.


The Integers Will Lie to You

Signed overflow is undefined behavior, not wraparound

Here is something the C++ standard says that most people refuse to believe: if a signed integer overflows, anything can happen. Not "it wraps to INT_MIN." Not "it gives a wrong answer." Anything. The compiler is allowed to assume overflow never occurs and optimize accordingly.

In optimized builds, the safety check you wrote may be deleted entirely because the compiler proved it "can't" trigger.

int a = INT_MAX; int b = a + 1; // UB. Not -2147483648. Anything can happen.

In practice this bites you on coordinate problems. Constraints say values are up to 10^9. The product fits in long long but not int. You multiply two int variables, silently overflow, get a plausible-looking negative number, and your solution produces wrong output on exactly the test case the interviewer runs.

long long area = (long long)width * height; // correct int area = width * height; // overflow when width=height=100000

The binary search midpoint is the same trap. mid = (lo + hi) / 2 overflows when both indices are large. Use lo + (hi - lo) / 2. This exact bug sat in Java's standard library for nine years. /blog/binary-search-off-by-one has the full story.

size() returns an unsigned type

vector::size() returns size_t, which is unsigned. Subtracting from it when the container is empty doesn't give you -1. It wraps around to 18,446,744,073,709,551,615.

vector<int> v; for (int i = 0; i < v.size() - 1; i++) { // v.size()-1 wraps to 18446744073709551615 // this loop runs a very long time }

This loop does not terminate quickly. Cast to int, check !v.empty() first, or just use range-based iteration and skip the arithmetic entirely.

Modulo with negatives bites Python users hardest

C++ truncates integer division toward zero, so (-7) % 3 == -1. Python floors toward negative infinity and gives 2. If you've practiced LeetCode in Python and you switch to C++ for an interview, this will surprise you exactly once. Hopefully not during the interview.

int mod(int a, int m) { return ((a % m) + m) % m; // always in [0, m-1] }

-INT_MIN is also undefined behavior

INT_MAX is 2,147,483,647. INT_MIN is -2,147,483,648. There is no positive int that equals -INT_MIN. When your algorithm needs to negate a value that might be INT_MIN, you have to cast first:

long long negated = -(long long)INT_MIN; // 2147483648, fits fine

Your Map Has Side Effects

operator[] inserts a default value on a missing key

This is the single most common C++ interview footgun. When you access a missing key in map<K,V> or unordered_map<K,V> via [], the map creates it with a zero-initialized value. Your innocent-looking if check just mutated your data structure.

unordered_map<string, int> counts; if (counts["apple"]) { // "apple" is now in the map with value 0 // ... } cout << counts.size(); // 1, not 0

You just added "apple" to a map you were trying to query. And since the value is zero, the if block didn't even execute. Perfectly silent. Completely wrong.

Use .find() or .count() to test membership. Use .at() if you want an exception when the key is absent.

if (counts.count("apple")) { /* safe */ } if (counts.find("apple") != counts.end()) { /* also safe */ }

unordered_map has no built-in hash for pair or tuple

unordered_map<pair<int,int>, int> does not compile. The standard provides hash specializations for basic types only. In a 45-minute interview, default to map<pair<int,int>, int> (O(log n), no extra setup required). If you genuinely need O(1), write a custom hasher:

struct PairHash { size_t operator()(const pair<int,int>& p) const { return hash<long long>()((long long)p.first << 32 | (unsigned)p.second); } }; unordered_map<pair<int,int>, int, PairHash> grid;

multiset::erase(val) removes every copy

Calling s.erase(5) on a multiset deletes all the 5s. Every single one. If you wanted to remove just one occurrence, you needed the iterator overload:

auto it = ms.find(5); if (it != ms.end()) ms.erase(it); // removes exactly one

The value overload is a trap that looks completely reasonable and silently corrupts your sliding window.


Modifying a Vector Breaks Your Iterators

You have a vector. You're looping over it. You erase an element. The iterator you're holding is now a dangling pointer to memory the vector no longer owns.

// Wrong: after erase, it is invalid for (auto it = v.begin(); it != v.end(); ++it) { if (*it % 2 == 0) v.erase(it); } // Correct: erase returns the next valid iterator for (auto it = v.begin(); it != v.end(); ) { if (*it % 2 == 0) { it = v.erase(it); } else { ++it; } }

Any push_back that triggers reallocation also invalidates every existing iterator. Any insert or erase at position k invalidates all iterators at k or later. The safest interview habit: never modify and iterate simultaneously. Collect what you want to remove, then remove it in a separate pass.


The Algorithm Names Are Lying to You

std::remove doesn't actually remove anything

I want you to read the name of this function and then learn that it does not remove elements from the container. std::remove shuffles the elements you want gone toward the end of the range and returns an iterator to where the garbage starts. The container's size is completely unchanged. The elements are still there.

v.erase(remove(v.begin(), v.end(), 5), v.end()); // correct remove(v.begin(), v.end(), 5); // 5s are still in v. Surprise.

This is not a bug. This is a design decision that has confused C++ developers for thirty years. You must pair it with .erase(). This pattern even has a name: the erase-remove idiom, because someone recognized that two functions were always needed to do the job of one.

std::unique requires the range to be sorted first

std::unique collapses consecutive duplicates. On an unsorted range, it only collapses adjacent equal elements and leaves the rest untouched.

vector<int> v = {1, 3, 1, 2}; sort(v.begin(), v.end()); // {1, 1, 2, 3} v.erase(unique(v.begin(), v.end()), v.end()); // {1, 2, 3}

Sort first. Then unique. You would think the name "unique" would imply something about uniqueness across the whole container, but you would be wrong.

std::sort is not stable

std::sort uses introsort. It does not preserve the relative order of equal elements. If you sort (name, score) pairs by score and two people tie, their order in the output is arbitrary. Use std::stable_sort when tie order matters. It's a one-word fix.

Your comparator must satisfy strict weak ordering or face UB

A custom comparator for std::sort must return false when comparing an element to itself. Using <= instead of < violates this rule and gives you undefined behavior: garbage output, a crash, or an infinite loop, with no error message to tell you why.

sort(v.begin(), v.end(), [](int a, int b) { return a <= b; }); // UB sort(v.begin(), v.end(), [](int a, int b) { return a < b; }); // correct

The same rule applies to comparators in map, set, and priority_queue.

lower_bound and upper_bound require a sorted range

Both are binary search operations. Run them on unsorted data and they silently return garbage. No assertion. No warning. Just wrong answers.


Your Heap Is Upside Down

priority_queue<int> is a max-heap by default. The largest element surfaces at top(). Every Dijkstra's implementation needs a min-heap, and every C++ developer has gotten this wrong at least once.

priority_queue<int, vector<int>, greater<int>> minHeap;

Two more non-obvious behaviors worth knowing:

  • No decrease_key operation. Once an element is inside the heap, you can't update its priority. The standard Dijkstra workaround is lazy deletion: push the updated entry and skip stale copies when they surface at the top.
  • top() and pop() are separate calls. Calling either on an empty queue is undefined behavior. Guard with !pq.empty().

Strings Are More Expensive Than They Look

Concatenation in a loop is O(n²)

s = s + token constructs a brand new string each iteration, copying everything from the previous string into the new one. Over n tokens, that's O(n²) total work. Use += or append(), which amortize to O(1) per call.

string s; for (const auto& token : tokens) { s += token; // O(1) amortized - correct // s = s + token; // O(n²) total - don't }

This matters when the test case is a long string and your solution times out. The interviewer will ask you to check your complexity. You will stare at your solution. You will not immediately see it.

substr copies, it doesn't slice

s.substr(i, k) allocates a new string and copies k characters. It's O(k), not O(1). Inside nested loops, that adds up to O(n²) or worse. C++17's string_view gives a zero-copy view into an existing string, but it becomes a dangling reference if the underlying string gets destroyed or reallocated.

Char arithmetic produces int

'a' + 1 has type int, not char. Fine in most contexts, but worth having in your head when building characters programmatically:

char c = 'a' + 3; // 'd', implicit narrowing, works int idx = someChar - 'a'; // fine, you want an int here anyway

Your Range Loop Has a Hidden Copy

Missing & copies every element

for (auto x : strings) { ... } // copies every string for (const auto& x : strings) { ... } // reads without copying - correct

For int this barely matters. For strings or vectors of vectors, the copy overhead is very real and will show up as TLE on large inputs.

vector<bool> is not a vector of bools

vector<bool> is a bitset masquerading as a standard container. Its operator[] returns a proxy object, not a bool&. This breaks code that takes addresses or references to elements. If you need an actual bool container, use vector<char> or deque<bool>.


C++ Coding Interview Quick Reference

TrapWhat actually happensFix
map[key] on missing keyInserts with default value.find() or .count()
v.size() - 1 on empty vectorWraps to ~2^64Check !v.empty() first
(lo + hi) / 2 in binary searchInteger overflowlo + (hi - lo) / 2
-7 % 3Returns -1((a % m) + m) % m
std::remove(...) aloneDoesn't resizePair with .erase()
std::unique(...) on unsorted rangeMisses non-adjacent duplicatesSort first
Comparator using <=Undefined behaviorUse strict <
priority_queue<int>Max-heapgreater<int> for min
s = s + token in loopO(n²) copiesUse +=
multiset::erase(val)Removes all copieserase(find(val))
unordered_map<pair<int,int>, V>Won't compileUse map or custom hash
Signed int overflowUndefined behaviorUse long long
-INT_MINUndefined behaviorCast to long long first

Voice-based mock interviews surface these bugs faster than grinding solo, because you have to narrate your reasoning as you write. The moment you say "map[key] to check if it exists," an interviewer flags it. SpaceComplexity runs C++ coding rounds with rubric-based feedback so you catch these footguns before they cost you an offer.

For a broader overview of the C++ standard library in interview contexts, see /blog/cpp-for-coding-interviews. The integer overflow problem comes up in binary search specifically: /blog/binary-search-off-by-one goes deep on that one case. For the time complexity guarantees behind the containers mentioned here, /blog/amortized-analysis covers the proofs.

Further Reading