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

- Signed integer overflow is undefined behavior in C++, not wraparound; cast to
long longbefore 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::removedoes not resize the container; always pair it with.erase()or the elements remain in placepriority_queue<int>is a max-heap by default; passgreater<int>as the comparator for min-heap behavior- String concatenation with
+in a loop is O(n²); use+=orappend()for amortized O(1) vector<bool>returns proxy objects, not real references; usevector<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_keyoperation. 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()andpop()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
| Trap | What actually happens | Fix |
|---|---|---|
map[key] on missing key | Inserts with default value | .find() or .count() |
v.size() - 1 on empty vector | Wraps to ~2^64 | Check !v.empty() first |
(lo + hi) / 2 in binary search | Integer overflow | lo + (hi - lo) / 2 |
-7 % 3 | Returns -1 | ((a % m) + m) % m |
std::remove(...) alone | Doesn't resize | Pair with .erase() |
std::unique(...) on unsorted range | Misses non-adjacent duplicates | Sort first |
Comparator using <= | Undefined behavior | Use strict < |
priority_queue<int> | Max-heap | greater<int> for min |
s = s + token in loop | O(n²) copies | Use += |
multiset::erase(val) | Removes all copies | erase(find(val)) |
unordered_map<pair<int,int>, V> | Won't compile | Use map or custom hash |
| Signed int overflow | Undefined behavior | Use long long |
-INT_MIN | Undefined behavior | Cast 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
- cppreference: Iterator invalidation rules - complete table by container and operation
- cppreference: Named requirements: Compare - strict weak ordering definition and examples
- cppreference: std::priority_queue - full interface, comparator signature
- SEI CERT C Coding Standard: INT32-C - authoritative reference on signed integer overflow rules
- cppreference: std::remove - clarifies what "remove" actually does and the erase-remove idiom