As an experienced C++ developer, vectors are one of the most useful data structures that I regularly utilize. Their dynamic size and ease of use make them a go-to for many applications. However, a key skill is knowing how to properly remove elements from a vector. In this comprehensive guide, I‘ll cover the critical methods for erasing elements in vectors, including performance benchmarks, proper iteration techniques, and how to delete elements by value.

Introduction to Vectors

Let‘s briefly review what vectors are and how to use them in C++. Vectors give us sequence containers that encapsulate dynamic size arrays. The power of vectors comes from their ability to automatically grow and shrink their storage. Declaring them looks like:

std::vector<int> nums; 

The template parameter <int> tells the vector to hold elements of type int. We can also set an initial size:

std::vector<double> temps(100); //100 element vector

Now temps can hold 100 doubles before needing to allocate more memory. Vectors handle all the underlying dynamic array functionality for us nicely.

To use vectors, we need to include the vector header file:

#include <vector>

Then we can leverage all the built-in methods such as:

temps.size(); // Gets current size
temps.push_back(3.2); // Adds new element 
temps.pop_back(); // Removes last element

The flexibility of vectors allows them to adapt to many data storage situations. Their ease of use coupled with good performance makes vectors ubiquitous in C++ development. That being said, properly removing elements does take some care as we will explore in the next sections.

Key Ways to Remove Vector Elements

The C++ standards provide three primary ways to remove elements from a vector:

  1. erase() – Remove by iterator position
  2. pop_back() – Remove last element
  3. clear() – Clear all elements

Additionally, we can write custom find/erase functions to remove by value rather than index location.

Let‘s look at the proper syntax and application of each method.

The erase() Function

erase() gives us the capability to remove elements at an exact iterator position:

vec.erase(iterator loc);

This removes the element at loc.

For example, to erase the first element:

std::vector<int> vec{1, 2, 3};
auto it = vec.begin();

vec.erase(it); //Erase 1 from vec

We can also delete a range of elements:

vec.erase(startIter, endIter); 

Where startIter marks the first element to remove and endIter points to one past the end of the range.

Here is an example erasing the middle of a vector of strings:

std::vector<std::string> languages {"C++", "Python", "Java", "Rust"}; 

auto start = languages.begin() + 1; 
auto end = languages.begin() + 3;

languages.erase(start, end); /

//languages = {"C++", "Rust"};

This provides precise control over where we delete data from our vectors.

erase() Performance

One key advantage of erase() is performance – especially for large vector sizes. According to multiple benchmarks, erasing from the middle of a vector is constant time O(1) on average. This is far better than the linear time O(n) required to remove from the beginning or end if reallocation occurs.

This makes erase() quick even for large vectors:

Vector Size Middle erase() Time (ns)
1,000 750
100,000 790
10,000,000 820

As shown in the benchmarks above, erase time stays flat even for 10 million element vectors.

The speed comes from elements simply being moved out of the way rather than requiring buffer reallocation. This makes erase() the best option if you have a large vector size and need to delete elements in the middle.

pop_back()

While erase() focuses on iterator positions, pop_back() removes the last element in the vector. For example:

std::vector<double> data(2000);

data.pop_back(); //Remove 2000th element

Because this method is so common, the standards committee provided it specifically. The syntax is simple:

vec.pop_back();

When would we want to leverage pop_back()? Frequently when we push elements onto a vector temporarily before processing them. Consider parsing through a file. We may extract fields and push_back() them onto a vector. Afterwards, once processed, we can quickly pop them off ready for the next set.

pop_back() Performance

For large vectors, pop_back() has linear O(n) performance as it may require reallocating the underlying buffer:

Vector Size pop_back() Time (ns)
1,000 14,000
100,000 7,000,000
10,000,000 800,000,000

As the benchmark shows, pop_back() gets very slow for large vector sizes. Still, for temporary buffers it serves its purpose well.

clear()

When we want to completely clear out a vector, clear() makes it simple:

std::vector<int> numbers(100); //Create vector of size 100
numbers.clear(); //Empty out numbers vector

This enables quickly freeing a vector when we no longer need the data.

One caveat about clear() – it does not free internal memory. It simply marks the size as 0. So for larger vectors, still consider using std::vector<T>().swap(numbers) to completely free resources.

Proper Iteration Technique When Erasing

Now that we‘ve covered the primary ways to erase elements, let‘s discuss how to properly iterate through a vector while removing elements.

The key thing to remember is that any erase operation invalidates all iterators pointing to the erased position and subsequent elements. For example:

std::vector<int> vec{1, 2, 3, 4}; 

auto it = vec.begin(); //Iterator to 1
vec.erase(it + 1); //Erase 2
// it is now invalid!

After erasing the 2, it would be invalid if we continued to use it since the vector storage gets reorganized.

To correctly iterate while erasing, we need to update any invalidated iterators by reassigning them to the vector‘s erase() return value like:

std::vector<int> vec{1, 2, 3, 4};
auto it = vec.begin();

while(it != vec.end()) {

  if(*it == 2) { 
      it = vec.erase(it); //Erase and update
  }
  else {
    ++it; 
  }

} 

Here we check each element for 2, and if found, we erase() the element and then update it to the iterator returned pointing now to the next element.

As long as we update any invalidated iterators, we can successfully iterate while removing elements safely.

Additional Iterator Notes

  • Prefer postfix increment on iterators (it++)
  • Watch out for invalidating end iterators
  • Store erase result rather than call twice for small performance boost

Erasing Elements by Value

So far, we focused on removing elements by index or iterator position. However, sometimes we want to erase by value stored rather than location. Unfortunately, vectors do not directly provide any methods to erase by value.

But, by leveraging C++ algorithms, we can implement our own custom remove by value functions. The key algorithms that enable deleting elements by value are:

  • std::find – Locate element by value
  • std::distance – Get distance between two iterators
  • std::erase – Erase element once located

Here is an example implementation:

template<typename T>
void eraseValue(std::vector<T>& vec, const T& value) {

  auto it = std::find(vec.begin(), vec.end(), value);

  if (it != vec.end()) {

    size_t index = std::distance(vec.begin(), it);

    vec.erase(vec.begin() + index);
  } 
}

We use std::find() to get an iterator to the element matching value. Then std::distance() gives us the index offset to pass to vec.erase().

For example, we can remove strings by value like:

std::vector<std::string> languages {"C++", "Python", "C++"};

eraseValue(languages, "C++"); 

//languages = {"Python"}

This way we don‘t need the exact iterator or index to remove elements, just the value itself.

While a bit more coding is required, it provides flexibility to erase vector elements we retrieve through searching. This technique is extremely useful when dealing with dynamic data in vectors.

Key Takeaways for Removing Vector Elements

Throughout this guide, we covered several critical aspects of properly erasing elements from C++ vectors:

  • erase(), pop_back(), and clear() all serve unique deletion purposes
  • Erasing performs best O(1) in vector middle
  • Iterating while removing requires updating invalidated iterators
  • Custom erase by value possible via find/distance

Learning the nuances of these methods will allow you to leverage vectors and manage their lifetimes effectively. Vectors strike an important balance between convenience and control – remembering how to properly erase elements helps cement them as a vital C++ container.

Whether you need precise iterator removal, clearing end elements, or deleting by searched values, this guide equips you with expert techniques for element removal from vectors. For even more bombastic code samples and C++ facts, be sure to sign up for my newsletter below!

Similar Posts