4 Ranges, Algorithms, and Lambdas

In Gorgo Starting C++ you learned to store data in std::vector and std::array and to iterate through them with range-based for loops. But when you need to sort, search, or filter that data, you end up writing loops by hand. Hand-written loops are repetitive, easy to get wrong (off-by-one errors, forgotten edge cases), and they bury your intent behind boilerplate. Every time you write a loop to find the maximum element, you are re-implementing logic that has already been written and tested. The standard library provides algorithms — pre-built, well-tested functions for the operations you need most. Combined with lambdas (inline functions you pass as arguments) and C++20 ranges, they let you say what you want done instead of spelling out how to do it. In this chapter you will learn the most common algorithms, how to write lambdas to customize their behavior, and how ranges and views make composing operations cleaner.

4.1 The <algorithm> Header

The <algorithm> header provides dozens of functions that operate on containers. Most of them take a pair of iterators (remember .begin() and .end() from Gorgo Starting C++) to define the range of elements to work on.

Let’s start with the most commonly used algorithms.

4.2 Sorting

std::sort arranges elements in ascending order:

void sort(Iterator first, Iterator last);
void sort(Iterator first, Iterator last, Compare comp);
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores = {88, 42, 95, 67, 73};

    std::sort(scores.begin(), scores.end());

    for (const auto& s : scores) {
        std::cout << s << " ";
    }
    std::cout << "\n";

    return 0;
}
42 67 73 88 95

It works with strings too — they sort alphabetically:

std::vector<std::string> songs = {"Hey Ya!", "Mr. Brightside", "Hips Don't Lie"};
std::sort(songs.begin(), songs.end());
// songs is now {"Hey Ya!", "Hips Don't Lie", "Mr. Brightside"}

4.3 Finding Elements

std::find searches for a value and returns an iterator to the first match, or .end() if not found:

Iterator find(Iterator first, Iterator last, const T& value);
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> playlist = {"Hey Ya!", "Mr. Brightside", "Hips Don't Lie"};

    auto it = std::find(playlist.begin(), playlist.end(), "Mr. Brightside");
    if (it != playlist.end()) {
        std::cout << "Found: " << *it << "\n";
    } else {
        std::cout << "Not found\n";
    }

    return 0;
}
Found: Mr. Brightside

Tip: Always check the result of std::find against .end() before dereferencing the iterator. Dereferencing .end() is undefined behavior.

4.4 Counting

std::count counts how many times a value appears:

typename iterator_traits<Iter>::difference_type
    count(Iter first, Iter last, const T& value);

The return type is the iterator’s difference_type — typically std::ptrdiff_t (a signed integer wide enough to hold any container size). You can store the result in auto to sidestep the long type name:

std::vector<int> votes = {1, 2, 1, 3, 1, 2, 1};
auto ones = std::count(votes.begin(), votes.end(), 1);   // ptrdiff_t
std::cout << "Number of 1s: " << ones << "\n";           // 4

4.5 for_each

std::for_each applies a function to every element in a range. It is similar to a range-based for loop, but is useful when you want to pass a function directly:

Function for_each(Iterator first, Iterator last, Function f);
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

void print_song(const std::string& song) {
    std::cout << "  " << song << "\n";
}

int main() {
    std::vector<std::string> songs = {"Hey Ya!", "Mr. Brightside"};

    std::cout << "Playlist:\n";
    std::for_each(songs.begin(), songs.end(), print_song);

    return 0;
}
Playlist:
  Hey Ya!
  Mr. Brightside

That third argument is a function — std::for_each calls it once for every element. But writing a separate named function for something this simple is tedious. This is where lambdas come in.

4.6 Lambdas

A lambda is an anonymous function that you define right where you need it. Instead of writing a separate function like print_song above, you can write:

std::for_each(songs.begin(), songs.end(), [](const std::string& song) {
    std::cout << "  " << song << "\n";
});

The lambda syntax is:

[captures](parameters) { body }
  • [captures] — what variables from the surrounding scope the lambda can access
  • (parameters) — just like function parameters
  • { body } — the code to execute

Here is a simple example:

auto greet = [](const std::string& name) {
    std::cout << "Hola, " << name << "!\n";
};

greet("Shakira");  // Hola, Shakira!
greet("OutKast");  // Hola, OutKast!

You can store a lambda in a variable using auto and call it like a regular function.

4.6.1 Captures

Lambdas can access variables from the surrounding scope through captures:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int threshold = 5;

    // std::count_if: like std::count, but with a predicate
    //   iterator difference_type count_if(Iter first, Iter last, Pred pred);
    auto count = std::count_if(nums.begin(), nums.end(),
        [threshold](int n) { return n > threshold; });

    std::cout << count << " numbers above " << threshold << "\n";

    return 0;
}
5 numbers above 5

The [threshold] capture makes a copy of threshold available inside the lambda.

Here are the capture options:

Syntax Meaning
[] Capture nothing
[x] Capture x by value (copy)
[&x] Capture x by reference
[=] Capture everything by value
[&] Capture everything by reference
[=, &x] Capture everything by value, but x by reference
int total = 0;
std::vector<int> prices = {10, 20, 30};

std::for_each(prices.begin(), prices.end(), [&total](int p) {
    total += p;
});

std::cout << "Total: " << total << "\n";  // Total: 60

Here [&total] captures total by reference, so the lambda can modify it.

Wut: By-value captures are const by default — a lambda that tries to modify [x] will not compile. Adding mutable after the parameter list ([x](int n) mutable { ... }) makes the lambda’s copies modifiable. Even then, the lambda modifies its own copy, not the original variable. If you want changes to reach the original, capture by reference ([&x]) instead.

Tip: Prefer specific captures like [x] or [&x] over blanket captures like [=] or [&]. Explicit captures make it clear what the lambda depends on and help prevent accidental captures.

4.7 Transform

std::transform applies a function to each element and stores the result. It can write the results back into the same container or into a different one:

OutputIterator transform(Iterator first, Iterator last, OutputIterator result, Function f);
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
    std::vector<int> doubled(nums.size());

    std::transform(nums.begin(), nums.end(), doubled.begin(),
        [](int n) { return n * 2; });

    for (const auto& d : doubled) {
        std::cout << d << " ";
    }
    std::cout << "\n";

    return 0;
}
2 4 6 8 10

Trap: When writing results to a different container, that container must already have enough space. In the example above, doubled is created with nums.size() elements. If you use an empty vector, you will write past its end — undefined behavior.

4.8 Accumulate

std::accumulate reduces a range to a single value by applying an operation. It lives in <numeric>, not <algorithm>:

T accumulate(Iterator first, Iterator last, T init);
T accumulate(Iterator first, Iterator last, T init, BinaryOp op);
#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> scores = {90, 85, 92, 88};

    int sum = std::accumulate(scores.begin(), scores.end(), 0);
    std::cout << "Sum: " << sum << "\n";
    std::cout << "Average: " << sum / static_cast<int>(scores.size()) << "\n";

    return 0;
}
Sum: 355
Average: 88

The third argument (0) is the initial value. You can also pass a custom operation as a fourth argument:

int product = std::accumulate(scores.begin(), scores.end(), 1,
    [](int a, int b) { return a * b; });

4.9 Min and Max

std::min_element and std::max_element return iterators to the smallest and largest elements:

Iterator min_element(Iterator first, Iterator last);
Iterator max_element(Iterator first, Iterator last);
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> temps = {72, 68, 85, 61, 79};

    auto coldest = std::min_element(temps.begin(), temps.end());
    auto hottest = std::max_element(temps.begin(), temps.end());

    std::cout << "Coldest: " << *coldest << "\n";
    std::cout << "Hottest: " << *hottest << "\n";

    return 0;
}
Coldest: 61
Hottest: 85

4.10 More Algorithms

<algorithm> and <numeric> provide far more than sort/find/for_each. A short tour of the rest of the everyday catalog — each is a one-liner replacement for a hand-rolled loop.

4.10.1 Searching Sorted Ranges

When a range is already sorted, three algorithms beat std::find’s linear scan:

bool binary_search(Iter first, Iter last, const T& v);   // is v present?
Iter lower_bound (Iter first, Iter last, const T& v);    // first not-less-than v
Iter upper_bound (Iter first, Iter last, const T& v);    // first greater-than v
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 3, 3, 5, 7, 9};      // sorted
    std::cout << std::binary_search(v.begin(), v.end(), 5) << "\n";    // 1
    auto lo = std::lower_bound(v.begin(), v.end(), 3);                  // points at first 3
    auto hi = std::upper_bound(v.begin(), v.end(), 3);                  // points at 5
    std::cout << "count of 3s: " << (hi - lo) << "\n";                  // 2
    return 0;
}

lower_bound plus upper_bound is how you find every occurrence of a value in a sorted range without touching anything else. The same pair underpins std::map::equal_range.

4.10.2 Predicate Checks

bool all_of (Iter first, Iter last, Pred pred);
bool any_of (Iter first, Iter last, Pred pred);
bool none_of(Iter first, Iter last, Pred pred);
std::vector<int> v = {2, 4, 6, 8};
auto even = [](int n) { return n % 2 == 0; };
std::cout << std::all_of (v.begin(), v.end(), even) << "\n";   // 1
std::cout << std::any_of (v.begin(), v.end(), even) << "\n";   // 1
std::cout << std::none_of(v.begin(), v.end(), even) << "\n";   // 0

These say what they mean. Reach for them whenever you find yourself writing “loop through, set a flag, break out.”

4.10.3 Copying and Moving

Iter copy(InIter first, InIter last, OutIter d_first);   // copy elements
Iter move(InIter first, InIter last, OutIter d_first);   // move them (cheaper when movable)

Both algorithms write into a pre-existing destination, so the destination must already have room (or you wrap it with std::back_inserter to grow as you go).

Wut: There are two functions named std::move in the standard library, and they do completely different things:

  • The algorithm std::move from <algorithm> (this section) takes an iterator range and moves every element from the source to the destination.
  • The cast std::move from <utility> (covered in Gorgo Starting C++ and revisited in this book’s RAII chapter) takes one value and casts it to an rvalue reference so the next operation can move from it.

The algorithm form takes three iterators; the cast form takes one value. They are simply two different functions that happen to share a name, and the compiler picks between them by the number and types of arguments. A human reader does not get that luxury, so in your own code prefer std::ranges::move (C++20+) when you mean the algorithm — it only ever names the algorithm.

4.10.4 Removing and Erasing

std::remove is famous for its misleading name: it does not remove anything from a container. It compacts the elements you want to keep to the front and returns an iterator to the new logical end — the container’s size is unchanged:

std::vector<int> v = {1, 2, 3, 2, 4, 2};
auto new_end = std::remove(v.begin(), v.end(), 2);
v.erase(new_end, v.end());                       // the actual erase --- "erase-remove"

C++20 collapses both lines into a single algorithm:

std::erase_if(v, [](int n) { return n == 2; });  // does both steps
std::erase   (v, 2);                              // value-based variant

Prefer std::erase / std::erase_if in new code — the older two-step idiom is everywhere in existing code, but it is easy to mis-call.

4.10.5 Rearranging

Iter partition  (Iter first, Iter last, Pred pred);   // matching elements first
Iter unique     (Iter first, Iter last);              // collapse adjacent duplicates
void reverse    (Iter first, Iter last);              // flip order in place
Iter rotate     (Iter first, Iter middle, Iter last); // make middle the new first
bool next_permutation(Iter first, Iter last);         // step to the next lex order
std::vector<int> v = {1, 2, 3, 4, 5};
std::partition(v.begin(), v.end(), [](int n) { return n % 2 == 0; });
// the evens come first, then the odds --- the order within each group
// is unspecified (std::stable_partition preserves it)

std::vector<int> sorted = {1, 1, 2, 2, 2, 3};
auto end = std::unique(sorted.begin(), sorted.end());
sorted.erase(end, sorted.end());                 // unique-erase idiom: {1, 2, 3}

std::reverse(v.begin(), v.end());

std::vector<int> p = {1, 2, 3};
do {
    for (int n : p) std::cout << n;
    std::cout << " ";
} while (std::next_permutation(p.begin(), p.end()));   // 123 132 213 231 312 321
std::cout << "\n";

std::unique only collapses adjacent duplicates — sort first if you want global deduplication.

4.10.6 Numeric Algorithms (<numeric>)

void iota(Iter first, Iter last, T value);        // fill with value, value+1, ...
T    gcd (T a, T b);                              // greatest common divisor
T    lcm (T a, T b);                              // least common multiple
#include <numeric>
#include <vector>

std::vector<int> v(10);
std::iota(v.begin(), v.end(), 1);                 // {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
std::cout << std::gcd(12, 18) << "\n";            // 6
std::cout << std::lcm(4, 6)   << "\n";            // 12

std::iota is the fastest way to seed a container with a sequence, and it pairs naturally with std::shuffle when you want a random permutation of [1..N].

4.11 Ranges (C++20)

The algorithms above all take pairs of iterators: container.begin(), container.end(). C++20 introduced the std::ranges namespace, which lets you pass the container directly:

#include <algorithm>
#include <iostream>
#include <ranges>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> songs = {"Hey Ya!", "Mr. Brightside", "Hips Don't Lie"};

    // Iter ranges::sort(Range& r);   // returns an iterator to the end of the range
    std::ranges::sort(songs);

    for (const auto& s : songs) {
        std::cout << s << "\n";
    }

    return 0;
}
Hey Ya!
Hips Don't Lie
Mr. Brightside

Compare std::sort(songs.begin(), songs.end()) with std::ranges::sort(songs). The ranges version is simpler and less error-prone — you cannot accidentally pass mismatched iterators.

std::ranges::find works the same way:

// Iterator ranges::find(Range& r, const T& value);
auto it = std::ranges::find(songs, "Mr. Brightside");
if (it != songs.end()) {
    std::cout << "Encontrado: " << *it << "\n";
}

Tip: When your compiler supports C++20, prefer std::ranges:: versions of algorithms. They are simpler, safer, and often provide better error messages when something goes wrong.

4.12 Views

Views are one of the most powerful features added in C++20. A view is a lightweight wrapper that transforms or filters elements lazily — it does not create a new container or copy data. Instead, it computes each element on demand as you iterate.

Think of it like looking through a filter: the original data does not change, you just see it differently.

4.12.1 Pipe Syntax

Views use the pipe operator | to chain operations together, much like Unix pipes:

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // auto views::filter(Predicate pred);   // keeps elements where pred is true
    for (int n : nums | std::views::filter([](int n) { return n % 2 == 0; })) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    return 0;
}
2 4 6 8 10

The expression nums | std::views::filter(...) creates a view that only yields even numbers. No new vector is created — the filter is applied on the fly as you iterate.

4.12.2 Common Views

Here are the views you will use most often:

auto views::filter(Predicate pred);      // keeps matching elements
auto views::transform(Function f);       // applies f to each element
auto views::take(int n);                 // first n elements
auto views::drop(int n);                 // skip first n elements
auto views::reverse;                     // iterate in reverse
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // filter: keep only elements matching a condition
    std::cout << "Even: ";
    for (int n : nums | std::views::filter([](int n) { return n % 2 == 0; })) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    // transform: apply a function to each element
    std::cout << "Tripled: ";
    for (int n : nums | std::views::transform([](int n) { return n * 3; })) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    // take: keep only the first N elements
    std::cout << "First 3: ";
    for (int n : nums | std::views::take(3)) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    // drop: skip the first N elements
    std::cout << "Skip 7: ";
    for (int n : nums | std::views::drop(7)) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    // reverse: iterate in reverse order
    std::cout << "Reversed: ";
    for (int n : nums | std::views::reverse) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    return 0;
}
Even: 2 4 6 8 10
Tripled: 3 6 9 12 15 18 21 24 27 30
First 3: 1 2 3
Skip 7: 8 9 10
Reversed: 10 9 8 7 6 5 4 3 2 1

4.12.3 More Views

The view catalog is wider than the five above:

auto views::iota(Start);                    // infinite sequence Start, Start+1, ...
auto views::iota(Start, End);               // bounded [Start, End)
auto views::split(Delimiter);               // split a range on a delimiter
auto views::join;                           // flatten a range-of-ranges
auto views::zip(Range...);                  // C++23: parallel walk
auto views::enumerate;                      // C++23: index + value pairs

iota is the lazy version of std::iota — no container needed:

#include <iostream>
#include <ranges>

int main() {
    for (int n : std::views::iota(1, 6)) {
        std::cout << n << " ";              // 1 2 3 4 5
    }
    std::cout << "\n";
    return 0;
}

split plus join give you a string-handling pair that does not allocate per piece:

#include <iostream>
#include <ranges>
#include <string>
#include <string_view>

int main() {
    std::string csv = "yeah,crazy,hey ya,toxic";

    for (auto piece : csv | std::views::split(',')) {
        std::cout << "[" << std::string_view(piece) << "]\n";
    }
    return 0;
}

Constructing a std::string_view directly from the subrange like this is C++23. In C++20, spell it std::string_view(piece.begin(), piece.end()).

C++23 adds views::zip and views::enumerate — two of the most-requested patterns from other languages:

std::vector<std::string> names  = {"Usher", "Lily Allen", "MGMT"};
std::vector<int>         years  = {2004, 2006, 2008};

for (auto [name, year] : std::views::zip(names, years)) {
    std::cout << name << " (" << year << ")\n";
}

for (auto [i, n] : std::views::enumerate(names)) {
    std::cout << i << ": " << n << "\n";
}

The structured binding pattern [a, b] works because each view’s “element” is a tuple-like proxy.

Tip: views::iota paired with views::zip replaces the “indexed for loop” idiom (for (int i = 0; i < v.size(); ++i)) with something both safer and more composable. The compiler still optimizes it to the same machine code in most cases.

Wut: Compiler support for the C++23 views (zip, enumerate, chunk, etc.) is still rolling out. You may need a recent GCC, Clang, or MSVC, and -std=c++23. The C++20 views (iota, split, join, plus the five from the previous section) are widely supported.

4.12.4 Chaining Views

The real power of views shows when you chain them together:

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Get the first 3 even numbers, doubled
    std::cout << "Result: ";
    for (int n : nums
            | std::views::filter([](int n) { return n % 2 == 0; })
            | std::views::transform([](int n) { return n * 2; })
            | std::views::take(3)) {
        std::cout << n << " ";
    }
    std::cout << "\n";

    return 0;
}
Result: 4 8 12

This reads almost like English: take nums, filter the even ones, double them, and take the first 3. Each | passes the result of the left side as input to the right side.

Wut: Views are lazy. In the chained example above, the transform and filter are not applied to all 10 elements. Once take(3) has yielded 3 elements, the pipeline stops — elements 8, 10, and beyond are never even looked at. This makes views very efficient when you only need a subset of results.

4.12.5 Views with Strings

Views work well with any container, including vectors of strings:

#include <iostream>
#include <ranges>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> songs = {
        "Hey Ya!", "Mr. Brightside", "Hips Don't Lie"
    };

    std::cout << "Long titles:\n";
    for (const auto& s : songs
            | std::views::filter([](const std::string& s) {
                return s.size() > 10;
            })) {
        std::cout << "  " << s << "\n";
    }

    return 0;
}
Long titles:
  Mr. Brightside
  Hips Don't Lie

4.13 Try It: Algorithm Starter

Here is a program that exercises several algorithms and views. Type it in, compile with g++ -std=c++23, and experiment:

#include <algorithm>
#include <iostream>
#include <numeric>
#include <ranges>
#include <string>
#include <vector>

int main() {
    std::vector<int> scores = {72, 95, 88, 61, 84, 90, 77};

    // Sort
    std::ranges::sort(scores);
    std::cout << "Sorted: ";
    for (int s : scores) {
        std::cout << s << " ";
    }
    std::cout << "\n";

    // Find
    // std::distance returns the number of steps between two iterators
    // as a signed difference type (typically ptrdiff_t):
    //   difference_type distance(Iterator first, Iterator last);
    auto it = std::ranges::find(scores, 88);
    if (it != scores.end()) {
        std::cout << "Found 88 at position "
                  << std::distance(scores.begin(), it) << "\n";
    }

    // Accumulate (still needs begin/end)
    int sum = std::accumulate(scores.begin(), scores.end(), 0);
    std::cout << "Sum: " << sum << ", Average: "
              << sum / static_cast<int>(scores.size()) << "\n";

    // Min and max --- ranges::minmax returns the smallest and largest elements:
    //   auto ranges::minmax(Range& r);   // returns {min, max}
    auto [lo, hi] = std::ranges::minmax(scores);
    std::cout << "Min: " << lo << ", Max: " << hi << "\n";

    // Lambda with count_if --- returns the range's signed difference type:
    //   difference_type ranges::count_if(Range& r, Predicate pred);
    int above_80 = std::ranges::count_if(scores,
        [](int s) { return s > 80; });
    std::cout << "Scores above 80: " << above_80 << "\n";

    // Views pipeline
    std::cout << "Top 3 scores doubled: ";
    for (int s : scores
            | std::views::reverse
            | std::views::take(3)
            | std::views::transform([](int s) { return s * 2; })) {
        std::cout << s << " ";
    }
    std::cout << "\n";

    return 0;
}

4.14 Key Points

  • The <algorithm> header provides reusable functions like std::sort, std::find, std::count, std::for_each, and std::transform.
  • std::accumulate (from <numeric>) reduces a range to a single value.
  • Lambdas are anonymous functions written as [captures](params) { body }. They are the primary way to customize algorithm behavior.
  • Captures control what a lambda can access from its surrounding scope: [x] by value, [&x] by reference, [=] all by value, [&] all by reference.
  • C++20 std::ranges:: algorithms accept containers directly instead of iterator pairs.
  • Views (std::views::filter, transform, take, drop, reverse) apply transformations lazily without copying data.
  • Views chain together with the | pipe operator, creating readable data pipelines.
  • Lazy evaluation means views only compute elements as they are consumed, making them efficient for large data sets.

4.15 Algorithm and View Cheat Sheet

A non-exhaustive list to keep handy:

  • search: find, find_if, binary_search, lower_bound, upper_bound
  • predicate: all_of, any_of, none_of, count, count_if
  • transform: transform, for_each
  • reduce: accumulate (<numeric>), reduce (parallel-aware)
  • sort & friends: sort, stable_sort, partial_sort, nth_element
  • rearrange: partition, unique, reverse, rotate, next_permutation, shuffle
  • copy/erase: copy, move, erase, erase_if, remove, remove_if
  • numeric (<numeric>): iota, gcd, lcm, accumulate, inner_product, partial_sum
  • views: filter, transform, take, drop, reverse, iota, split, join, zip (C++23), enumerate (C++23)

When you find yourself writing a hand loop, scan this list first. A named algorithm beats a custom loop on readability, correctness, and usually performance.

4.16 Exercises

  1. Think about it: Why do you think the standard library provides both std::sort(v.begin(), v.end()) and std::ranges::sort(v)? If the ranges version is simpler, why keep the iterator version?

  2. What does this print?

    std::vector<int> v = {5, 3, 8, 1, 9, 2};
    std::sort(v.begin(), v.end());
    auto it = std::find(v.begin(), v.end(), 8);
    std::cout << *it << " " << *(it - 1) << "\n";
  3. What does this print?

    std::vector<int> v = {1, 2, 3, 4, 5};
    auto result = std::count_if(v.begin(), v.end(),
        [](int n) { return n % 2 != 0; });
    std::cout << result << "\n";
  4. Where is the bug?

    std::vector<int> nums = {10, 20, 30};
    std::vector<int> doubled;
    
    std::transform(nums.begin(), nums.end(), doubled.begin(),
        [](int n) { return n * 2; });
  5. Calculation: Given this code:

    std::vector<int> v = {4, 7, 2, 9, 1};
    int x = std::accumulate(v.begin(), v.end(), 10);

    What is the value of x?

  6. What does this print?

    int factor = 3;
    auto multiply = [factor](int n) { return n * factor; };
    std::cout << multiply(5) << " " << multiply(10) << "\n";
  7. Where is the bug?

    std::vector<int> nums = {1, 2, 3, 4, 5};
    int total = 0;
    
    std::for_each(nums.begin(), nums.end(), [total](int n) mutable {
        total += n;
    });
    
    std::cout << "Total: " << total << "\n";
  8. Think about it: Views are “lazy” — they do not process elements until you iterate. Why is this an advantage? Can you think of a situation where processing all elements upfront would be better?

  9. What does this print? (Assume C++20)

    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
    for (int n : v
            | std::views::filter([](int n) { return n > 3; })
            | std::views::take(3)) {
        std::cout << n << " ";
    }
    std::cout << "\n";
  10. Write a program that stores a list of test scores in a std::vector<int>, then uses algorithms and/or views to:

    • Sort the scores
    • Print only scores above 70
    • Print the average score
    • Print the highest and lowest scores
  11. What does this print?

    #include <algorithm>
    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
        std::sort(v.begin(), v.end());
        auto end = std::unique(v.begin(), v.end());
        v.erase(end, v.end());
        for (int n : v) std::cout << n << " ";
        std::cout << "\n";
        return 0;
    }

    Why is the call to std::sort essential here? What does v look like if you delete the sort line?

  12. Where is the bug?

    std::vector<int> v = {1, 2, 3, 4};
    std::remove(v.begin(), v.end(), 2);
    for (int n : v) std::cout << n << " ";

    What does the program print, what was the programmer probably expecting, and what is the canonical fix?

  13. What does this print?

    #include <algorithm>
    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> sorted = {1, 3, 3, 3, 5, 7, 9};
        auto lo = std::lower_bound(sorted.begin(), sorted.end(), 3);
        auto hi = std::upper_bound(sorted.begin(), sorted.end(), 3);
        std::cout << (hi - lo) << "\n";
        return 0;
    }

    Explain the difference between lower_bound and upper_bound.

  14. Calculation: Use std::iota (from <numeric>) to fill a std::vector<int> of size 10 with 1, 2, ..., 10. Then use std::accumulate (also <numeric>) to sum it. What value does it produce, and why does it match the formula n(n+1)/2?

  15. What does this print? (Assume C++23)

    #include <iostream>
    #include <ranges>
    #include <string>
    #include <vector>
    
    int main() {
        std::vector<std::string> tracks = {"Crazy", "Toxic", "Hey Ya"};
        for (auto [i, name] : std::views::enumerate(tracks)) {
            std::cout << i << ": " << name << "\n";
        }
        return 0;
    }

    What two things does views::enumerate produce for each step? Why does the structured binding [i, name] work?