STL Containers and Algorithms

STL Containers and Algorithms

Combine containers, iterators, and algorithms to write expressive code with less manual looping.

STL Containers and Algorithms

Start with std::vector

std::vector<int> values{5, 2, 8, 1};
std::sort(values.begin(), values.end());

Find and count

auto it = std::find(values.begin(), values.end(), 8);
auto zeros = std::count(values.begin(), values.end(), 0);
auto passing = std::count_if(values.begin(), values.end(), [](int value) {
    return value >= 50;
});

find_if and count_if are often the first algorithms you reach for once the condition depends on more than simple equality.

Transform

std::vector<int> doubled(values.size());
std::transform(values.begin(), values.end(), doubled.begin(),
               [](int x) { return x * 2; });

Algorithms shine when the operation is clearer than a manual loop. You describe the transformation instead of writing the loop mechanics again.

Main lesson

Think in terms of operations on a range, not always in terms of manual index loops.

Filtering and aggregation example

std::vector<int> scores{91, 84, 76, 99, 88};
std::vector<int> passing;

std::copy_if(scores.begin(), scores.end(), std::back_inserter(passing),
             [](int score) { return score >= 80; });

int total = std::accumulate(passing.begin(), passing.end(), 0);

This is a realistic pattern: select a subset, then compute something over it.

Larger worked example

struct Record {
    std::string name;
    int score{};
};

std::vector<Record> records{{"Ada", 91}, {"Bjarne", 78}, {"Grace", 95}};

std::vector<std::string> top_names;
std::for_each(records.begin(), records.end(), [&](const Record& record) {
    if (record.score >= 90) {
        top_names.push_back(record.name);
    }
});

std::ranges::sort(top_names);

This combines selection, transformation, and final ordering. In real code, algorithms often appear in short sequences rather than one isolated call at a time.

The erase-remove pattern

values.erase(std::remove(values.begin(), values.end(), 0), values.end());

Older STL algorithms often rearrange elements but do not actually shrink the container. Learn this pattern early.

std::erase(values, 0);

In newer standard library code, prefer std::erase or std::erase_if when available because they express the intent directly.

Modern ranges form

std::ranges::sort(values);
auto found = std::ranges::find(values, 8);

The ranges overloads are easier to read because they work directly with containers.

Working with objects

struct User {
    std::string name;
    int score{};
};

std::vector<User> users{{"Ada", 91}, {"Bjarne", 88}, {"Grace", 99}};
std::ranges::sort(users, std::greater<>{}, &User::score);

Projections let you target a member directly without building a new comparator every time.

Useful next algorithms

Projection example

When sorting objects, you can project on a member instead of writing a custom comparator for every case.

Common mistakes

Practical rule of thumb

When the loop is really about searching, filtering, transforming, or checking a condition, look for an algorithm first. When the loop is maintaining a more complex invariant across several moving parts, a normal loop may still be clearer.

Example in practice

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> values{4, 1, 3, 2};
    std::sort(values.begin(), values.end());
    return std::binary_search(values.begin(), values.end(), 3) ? 0 : 1;
}

Try this variation

Replace the hand-written search loop with a ranges pipeline. It highlights when composition improves clarity and when it becomes too indirect.

#include <ranges>
#include <vector>

int main() {
    std::vector<int> values{1, 2, 3, 4, 5, 6};
    auto odd = values | std::views::filter([](int value) { return value % 2 == 1; });
    return *odd.begin();
}