remove()
, remove_if()
, remove_copy()
, and remove_copy_if()
.In this lesson, we cover the four main C++ standard library algorithms for removing objects from our containers. The functions we will cover are: remove()
, remove_if()
, remove_copy()
, and remove_copy_if()
.
remove()
searches our range for specific values, and removes any it finds.remove_if()
passes the objects of our range to a predicate function. If that function returns true
, the object is removed from the range.remove_copy()
combines the copy()
and remove()
algorithms. It copies objects from a source range to a new location. However, if an object matches a specific value, it is not copied.remove_copy_if()
combines the copy()
and remove_if()
algorithms. It passes every object in our source range to a predicate function. If that function returns true
, our object is copied to a destination range. Otherwise, it isn’t copied.We also explain why the remove algorithms don’t necessarily erase items from the container, and how to address that using the remove-erase idiom.
All the algorithms in this section are available within the <algorithm>
 header:
#include <algorithm>
std::ranges::remove()
The most basic use of the std::ranges::remove()
algorithm involves two arguments:
In the following example, we request the removal of any 3
s from our Source
 range:
std::vector Source{1, 2, 3, 4, 5, 6};
std::ranges::remove(Source, 3);
Each object in our collection is compared to 3
using the equality operator ==
. If that operator returns true
, the object is removed.
For reasons we’ll cover later in this lesson, the removal algorithms cannot modify the size of our container. Instead, all the objects that were not removed are clustered at the start of our container, potentially with surplus elements at the end.
Note the additional 6
at the end of our container after running the remove()
 algorithm:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 4, 5, 6};
std::ranges::remove(Source, 3);
std::cout << "Objects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
}
Objects in Source: 1, 2, 4, 5, 6, 6,
We explain why remove()
works this way, and how to erase the surplus elements in the next section. For now, let's finish our exploration of std::ranges::remove()
The number of surplus objects on the right of our container is equal to the number of elements we removed. In the above example, we removed 1 object, so we have only one surplus object at the end.
Elements are moved left based on move semantics, and the surplus elements at the end of the container are in the moved-from state. We define what move semantics, and the moved-from state are in a dedicated lesson:
std::ranges::remove()
returns a subrange that contains all the surplus elements on the right of the container. Below, we use the returned subrange for some follow-up operations:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 4, 5, 6};
auto Surplus{
std::ranges::remove(Source, 3)};
std::cout << "Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
std::cout << "\nSurplus: ";
for (auto Num : Surplus) {
std::cout << Num << ", ";
}
std::cout << "\nQuantity Removed: "
<< Surplus.size();
std::cout << "\nObjects to Keep: ";
for (auto Num : std::ranges::subrange{
Source.begin(), Surplus.begin()}) {
std::cout << Num << ", ";
}
}
Source: 1, 2, 4, 5, 6, 6,
Surplus: 6,
Quantity Removed: 1
Objects to Keep: 1, 2, 4, 5, 6,
Let's cover why the removal algorithms leave surplus elements at the end of the container. Iterators provide a standardized way to traverse through containers, and access objects within containers. However, they are much more limited in what they can do to the container itself.
Specifically, they cannot erase objects from a container. How we erase objects from a container (and therefore, resize the container) depends on the type of container it is. It’s not something that is in the purview of ranges or iterators.
The ability to erase something from a container is generally provided as a method on that container type.
For example, the erase()
method on std::vector
lets us pass an iterator pair, denoting the start and end of the subrange we want to erase. Conveniently, as we saw above, this is exactly what the std::ranges::remove()
algorithm returns. Below, we use this to erase the surplus elements from our std::vector
 container:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 3, 3, 4, 5};
auto Subrange{
std::ranges::remove(Source, 3)};
Source.erase(Subrange.begin(), Subrange.end());
std::cout << "Objects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
}
Objects in Source: 1, 2, 4, 5,
The std::ranges::subrange
type supports structured binding, giving us easier access to the iterator and sentinel. Therefore, the previous example can be rewritten like this:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 3, 3, 4, 5};
auto [begin, end]{
std::ranges::remove(Source, 3)};
Source.erase(begin, end);
std::cout << "Objects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
}
Other containers, including standard library linked lists, also have erase()
methods that work similarly. This pattern of moving the elements we want to keep to the left of a container, and then truncating it, is so common, it has its own name: the remove-erase idiom.
The approach may seem like an indirect and inefficient way to remove items, but this tends not to be the case. Erasing an object from a container often involves resizing the container and potentially moving a lot of other objects to a new position. Moreover, when we’re using an algorithm like std::ranges::remove()
, we’re probably removing multiple objects.
For most types of containers, moving unwanted objects to the end of our container, and then getting rid of them all in a single erase operation is the most performant approach. This is particularly true for arrays, such as std::vector
, which tend to be the container type we’re using most often.
Therefore, unless there’s a specific reason to do otherwise, the remove-erase idiom is generally an effective approach.
std::ranges::remove_if()
The remove_if()
algorithm works similarly to remove()
but our second argument is a predicate function, rather than an object to run an equality check against. If the predicate returns true
for an object, that object will be removed from the input range.
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 4, 5, 6};
auto isEven{[](int x) { return x % 2 == 0; }};
std::ranges::remove_if(Source, isEven);
std::cout << "Objects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
}
Similar to remove()
the remove_if()
algorithm will not erase anything. Rather, it will ensure the objects we want to keep are at the beginning of the container. In the previous example, we want to keep the odd numbers, ie 1
, 3
, and 5
, so those are now at the left edge of our source:
Objects in Source: 1, 3, 5, 4, 5, 6,
The remove_if()
algorithm returns a subrange within our original container. The subrange will be on the right side of our container and will contain the part we want to erase. In this example, the elements to erase are the last three:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 4, 5, 6};
auto isEven{[](int x) { return x % 2 == 0; }};
auto Result{
std::ranges::remove_if(Source, isEven)};
std::cout << "Objects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
std::cout << "\nObjects to Erase: ";
for (auto Num : Result) {
std::cout << Num << ", ";
}
}
Objects in Source: 1, 3, 5, 4, 5, 6,
Objects to Erase: 4, 5, 6,
We can then use container-specific algorithms to do that erasing. For example, if our original container was a std::vector
, we can use the erase()
method of that class:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 4, 5, 6};
auto isEven{[](int x) { return x % 2 == 0; }};
auto [begin, end]{
std::ranges::remove_if(Source, isEven)};
Source.erase(begin, end);
std::cout << "Objects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
}
Objects in Source: 1, 3, 5,
std::ranges::remove_copy()
The std::ranges::remove_copy()
algorithm combines the copy()
and remove()
algorithms. Objects are copied from one location to another; however, objects that pass an equality check with a value we provide are not copied.
In its typical usage, the std::ranges::remove_copy()
algorithm has three arguments:
Below, we copy objects that are not equal to 3
- in other words, we copy only the objects where Object == 3
return false
:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 4, 5, 6};
std::vector Destination{0, 0, 0, 0, 0, 0};
std::ranges::remove_copy(
Source, Destination.begin(), 3);
std::cout << "Objects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
std::cout << "\nObjects in Destination: ";
for (auto Num : Destination) {
std::cout << Num << ", ";
}
}
Objects in Source: 1, 2, 3, 4, 5, 6,
Objects in Destination: 1, 2, 4, 5, 6, 0,
The algorithm will return a std::in_out_result
, aliased as std::remove_copy_result
. This is a struct with two properties:
in
- an iterator for our source range, pointing to where the sentinel was found. In the above example, this will be equivalent to Source.end()
out
- an iterator for our destination location, pointing beyond the last element that was copied.Below, we use this return value for some logging operations, and to trim our destination container to only contain the elements that were copied:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{1, 2, 3, 4, 5, 6};
std::vector Destination{0, 0, 0, 0, 0, 0};
auto [in, out]{std::ranges::remove_copy(
Source, Destination.begin(), 3)};
std::cout << "The source had "
<< std::distance(Source.begin(), in)
<< " elements.\n";
std::cout << std::distance(
Destination.begin(), out)
<< " elements were copied.";
Destination.erase(out, Destination.end());
std::cout << "\nObjects in Destination: ";
for (auto Num : Destination) {
std::cout << Num << ", ";
}
}
The source had 6 elements.
5 elements were copied.
Objects in Destination: 1, 2, 4, 5, 6,
std::ranges::remove_copy_if()
The remove_copy_if()
algorithm works in the same way as remove_copy()
- the only difference is that rather than using an equality check to determine which objects are excluded from the copy, we instead use a predicate function.
The basic usage of the algorithm requires three arguments:
Each object in our source range is passed to our predicate function. If that function returns false
, the object is copied to the destination location. If it returns true
, the object is excluded, effectively removing it from the destination location.
Below, we use remove_copy_if()
to copy objects to a destination, unless they are greater than or equal to 2
:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{-3, -2, -1, 0, 1, 2, 3};
std::vector Destination{0, 0, 0, 0, 0, 0, 0};
auto Predicate{[](int x) { return x >= 2; }};
std::ranges::remove_copy_if(
Source, Destination.begin(), Predicate);
std::cout << "\nObjects in Source: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
std::cout << "\nObjects in Destination: ";
for (auto Num : Destination) {
std::cout << Num << ", ";
}
}
Objects in Source: -3, -2, -1, 0, 1, 2, 3,
Objects in Destination: -3, -2, -1, 0, 1, 0, 0,
Similar to remove_copy()
, the remove_copy_if()
algorithm returns a std::in_out_result
, aliased to std::remove_copy_if_result
. This is a struct with two properties:
in
- an iterator for the input range, pointing to where the sentinel was found. In the previous example, this will be equivalent to Source.end()
out
- an iterator for the destination location, pointing beyond the last element that was copiedThis return value may or may not be useful, depending on our needs. Typically, when we do need to use it, we’ll use structured binding to access the two properties directly.
In this example, we show how it can be used with std::distance()
to get some information about our input range, and the number of objects that were copied. We then use it to trim our destination container to only contain the elements that were copied:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{-3, -2, -1, 0, 1, 2, 3};
std::vector Destination{0, 0, 0, 0, 0, 0, 0};
auto Predicate{[](int x) { return x >= 2; }};
auto [in, out]{std::ranges::remove_copy_if(
Source, Destination.begin(), Predicate)};
std::cout << "The source had "
<< std::distance(Source.begin(), in)
<< " elements.\n";
std::cout << std::distance(
Destination.begin(), out)
<< " elements were copied.";
Destination.erase(out, Destination.end());
std::cout << "\nObjects in Destination: ";
for (auto Num : Destination) {
std::cout << Num << ", ";
}
}
The source had 7 elements.
5 elements were copied.
Objects in Destination: -3, -2, -1, 0, 1,
As with most range-based algorithms, every function we covered in this lesson has an overload that accepts a projection function.
In the following example, we provide a projection function to remove()
that projects objects in our input range to their absolute value. As a result, both the -2
and 2
are projected to 2
. Both of these projections pass an equality check with the second argument we provide, and therefore both the -2
and the 2
are removed:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{-3, -2, -1, 0, 1, 2, 3};
auto Projector{[](int x) {
return std::abs(x); }};
auto Subrange{
std::ranges::remove(Source, 2, Projector)};
std::cout << "Objects in Destination: ";
for (auto Num : std::ranges::subrange{
Source.begin(), Subrange.begin()}) {
std::cout << Num << ", ";
}
}
Objects in Destination: -3, -1, 0, 1, 3,
In this more complex example, we use the remove_copy_if()
algorithm with a member function as our projector. This function projects our Player
objects to their level. This level is then received by our predicate function, which returns true
if the Level
is less than 15
The net effect is that any Player
below level 15
is not copied to the destination:
#include <algorithm>
#include <iostream>
#include <vector>
class Player {
public:
int GetLevel() { return Level; }
std::string GetName() { return Name; }
std::string Name{"(None)"};
int Level;
};
int main() {
std::vector<Player> Players;
Players.emplace_back("Roderick", 10);
Players.emplace_back("Anna", 20);
Players.emplace_back("Robert", 30);
std::vector<Player> Destination;
Destination.resize(Players.size());
auto[in, out]{std::ranges::remove_copy_if(
Players,
Destination.begin(),
[](int Level) { return Level < 15; },
&Player::GetLevel
)};
Destination.erase(out, Destination.end());
std::cout << "Copied: ";
for (Player& P : Destination) {
std::cout << P.GetName() << " (Level "
<< P.GetLevel() << "), ";
}
}
Copied: Anna (Level 20), Robert (Level 30),
As usual, the range-based removal algorithms covered in this lesson allow us to define our input ranges as iterator-sentinel pairs, rather than a single argument.
Below, we use this technique to remove all the 0
s in our container, excluding the first and last elements:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{0, 1, 0, 1, 0};
auto Removed{std::ranges::remove(
Source.begin() + 1, Source.end() - 1, 0)};
Source.erase(Removed.begin(), Removed.end());
std::cout << "Removed " << Removed.size()
<< " object(s)";
std::cout << "\nObjects remaining: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
}
Removed 1 object(s)
Objects remaining: 0, 1, 1, 0,
The algorithms in this lesson use the range concepts introduced in C++20. In earlier specifications, we have variations of these algorithms that work directly with iterators. These are available by excluding the ranges
namespace from the identifier.
For example, instead of using std::ranges::remove()
, we could use std::remove()
Compared to the range-based counterparts, the older iterator variations of these algorithms have a few disadvantages:
Additionally, the std::remove()
and std::remove_if()
algorithms return an iterator pointing beyond the last element of our new range. Everything from that iterator to the end of the input represents surplus objects that we can erase.
This return type is less friendly than the range-based counterparts, which represent the surplus elements as a subrange. Below, we rewrite the previous example to use std::remove()
rather than std::ranges::remove()
:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector Source{0, 1, 0, 1, 0};
auto End{std::remove(
Source.begin() + 1, Source.end() - 1, 0)};
ptrdiff_t RemovedCount {
std::distance(End, Source.end() - 1)};
std::cout << "Removed "
<< RemovedCount << " object(s)";
Source.erase(End, End + RemovedCount);
std::cout << "\nObjects remaining: ";
for (auto Num : Source) {
std::cout << Num << ", ";
}
}
Removed 1 object(s)
Objects remaining: 0, 1, 1, 0,
In this lesson, we explored the removal algorithms provided by the C++ standard library. We covered:
remove()
, remove_if()
, remove_copy()
, and remove_copy_if()
algorithms.An overview of the key C++ standard library algorithms for removing objects from containers. We cover remove()
, remove_if()
, remove_copy()
, and remove_copy_if()
.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.