C++
std::remove
vector::erase
undefined behavior
programming

stdremove with vectorerase and undefined behavior

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Many C++ bugs around std::remove come from a simple misconception: std::remove does not erase elements from a container. It only reorders the range and returns an iterator to the new logical end. Actual removal requires a second call to vector::erase, and iterator invalidation rules must be respected to avoid undefined behavior.

Understanding the Remove-Erase Idiom

The remove-erase idiom is a two-step algorithm. First, std::remove compacts values you want to keep toward the front of the vector. Second, erase truncates the trailing portion.

cpp
1#include <algorithm>
2#include <iostream>
3#include <vector>
4
5int main() {
6    std::vector<int> numbers{1, 2, 3, 2, 4, 2, 5};
7
8    auto new_end = std::remove(numbers.begin(), numbers.end(), 2);
9    // numbers size is still 7 here
10
11    numbers.erase(new_end, numbers.end());
12
13    for (int value : numbers) {
14        std::cout << value << ' ';
15    }
16    std::cout << '\n';
17}

In this example, the vector ends with 1 3 4 5. The key point is that remove works on iterators and values, not container size. That design allows it to operate on many sequence types.

Why Undefined Behavior Happens

The idiom itself is safe. Undefined behavior appears when code keeps using iterators or references that became invalid after an erase or reallocation. For std::vector, erasing at position p invalidates iterators and references at p and after it.

cpp
1#include <vector>
2
3int main() {
4    std::vector<int> v{10, 20, 30, 40};
5    auto it = v.begin() + 2; // points to 30
6
7    v.erase(v.begin());      // invalidates it
8
9    // Undefined behavior: using invalidated iterator
10    int x = *it;
11    (void)x;
12}

A similar bug occurs when you cache pointers to vector elements and then grow the vector. Any reallocation invalidates all pointers, references, and iterators.

Correct Patterns While Iterating

When removing while iterating, use the iterator returned by erase. Do not increment blindly after erasing.

cpp
1#include <iostream>
2#include <vector>
3
4int main() {
5    std::vector<int> values{5, -1, -4, 7, -3, 8};
6
7    for (auto it = values.begin(); it != values.end();) {
8        if (*it < 0) {
9            it = values.erase(it); // next valid iterator
10        } else {
11            ++it;
12        }
13    }
14
15    for (int v : values) {
16        std::cout << v << ' ';
17    }
18    std::cout << '\n';
19}

For “remove by value” and “remove by predicate”, prefer remove-erase or the C++20 helpers std::erase and std::erase_if.

cpp
1#include <vector>
2#include <algorithm>
3
4int main() {
5    std::vector<int> v{1, 2, 3, 4, 5, 6};
6
7#if __cplusplus >= 202002L
8    std::erase_if(v, [](int x) { return x % 2 == 0; });
9#else
10    v.erase(
11        std::remove_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }),
12        v.end()
13    );
14#endif
15}

Subtle Cases You Should Test

Even correct-looking code can fail on edges that are easy to miss in review. Add tests for these scenarios:

  • empty vector input
  • no matching elements
  • all elements removed
  • long runs of consecutive matching elements
  • predicates with mutable external state

A frequent production failure is when code assumes at least one element remains after filtering. Always handle the “all removed” case before indexing.

Performance Notes

Repeated single-element erases inside a loop can be expensive because each erase shifts elements. Remove-erase generally performs fewer moves and is easier for compilers to optimize.

If removal is your dominant operation and stable iterators are required, std::vector may be the wrong container. You might consider std::list for iterator stability or std::deque for different invalidation behavior, though each has tradeoffs in memory layout and cache efficiency.

For performance-sensitive work, benchmark realistic data sizes and patterns instead of assuming one approach is always faster.

Common Pitfalls

  • Assuming std::remove changes vector size.
  • Forgetting to call erase(new_end, end) after remove.
  • Using iterators or references after erase invalidates them.
  • Erasing in a loop without reassigning the iterator from erase.
  • Caching pointers to vector elements across operations that may reallocate.

Summary

  • 'std::remove compacts elements and returns a logical end iterator.'
  • 'vector::erase is the operation that actually shrinks the container.'
  • Undefined behavior usually comes from invalid iterator or reference usage, not from remove-erase itself.
  • The safest mutation pattern while iterating is to use the iterator returned by erase.
  • C++20 std::erase and std::erase_if reduce boilerplate and improve clarity.
  • Test edge cases explicitly to catch invalid assumptions before production.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.