Iterators, Algorithms, and Ranges

Filtering and Transforming with Views

Many sequences contain many more elements than any given computation requires, or it may be the correct elements but in a different form. Instead of creating a new container that copies only the parts that we want, with C++20 ranges we can express the filtering or transformation as a lightweight view. A view refers to the source data and is used only when it gives its result.

Starting with the following vector of integers, we have a requirement: analyze only the even integers. To do this, we need to refer to a named predicate function.

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

bool isEven(int n)
{
    return n % 2 == 0;
}

int main()
{
    std::vector v{ 1, 2, 3, 4, 5, 6, 7, 8 };

    // Build a lazy view instead of copying elements
    auto evens{ v | std::views::filter(isEven) };

    for (int n : evens)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

Our function isEven takes one integer as argument and returns true if the argument is divisible by 2. When we then combine v with the range adaptor std::views::filter, with isEven we get a view that refers to v, while every element for which isEven has returned false would be skipped. Let this view be called evens.

Every time a range based loop moves the evens view ahead, the range adaptor would evaluate the isEven function on the respective elements of v and return solely the ones for which the functions resulted in true values.

The program would thus output: 2 4 6 8. The original vector would not change at all, instead just parts of its contents were selected.

Another range adaptor could be used to create another stream of values, by transforming every existing element. Similarly, we would express this transformation with a named function, that converts from one type to the other:

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

int doubleValue(int n)
{
    return n * 2;
}

int main()
{
    std::vector v{ 1, 2, 3, 4, 5 };

    // Pipe the range through this view adaptor
    auto doubled{ v | std::views::transform(doubleValue) };

    for (int n : doubled)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

The doubleValue function would take an int as argument and return double the value of it. Similarly, by combining this function with range adaptor std::views::transform, a view that calls the doubleValue function for each of the elements within v and returns those resulted values would be generated. Iteration over this combined view:

would result in the following output: 2 4 6 8 10. Just as in the previous case, no new memory was allocated and original contents remained untouched: double values would only be computed on request.

These two functions could be used together to filter and transform the sequence. By “piping” one into the other:

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

bool isEven(int n)
{
    return n % 2 == 0;
}

int doubleValue(int n)
{
    return n * 2;
}

int main()
{
    std::vector v{ 1, 2, 3, 4, 5, 6, 7, 8 };

    // Store the value produced by this function call
    auto result{ v | std::views::filter(isEven) | std::views::transform(doubleValue) };

    for (int n : result)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

The first adaptor will take only even values out of the source vector v. The second one will convert the surviving elements. We would name the combined view with result and finally:

would yield the following result: 4 8 12 16. Execution would be equally deferred as the call to isEven occurs upon access and doubleValue is then evaluated with a valid filtered element. We never used and therefore no memory will be allocated for any intermediate results.

Since the elements of a view are never owned, it implies the underlying data that it refers to should at least outlive every view referring to it. The code example:

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

bool isEven(int n)
{
    return n % 2 == 0;
}

auto makeFilteredView()
{
    std::vector v{ 1, 2, 3, 4, 5, 6 };
    return v | std::views::filter(isEven);
}

int main()
{
    // Initialize evens with the value returned by makeFilteredView
    auto evens{ makeFilteredView() };
    std::cout << *evens.begin() << '\n';

    return 0;
}

would violate this crucial rule. Inside the makeFilteredView function a local vector is declared and an adaptor view referencing this local vector is generated. After the function returns, the vector's scope ends and all of its contents (storage included) would be freed.

The adaptor still refers to data that is already destroyed and therefore using this adaptor results in undefined behavior!

Any view, whatsoever it may be, that relies upon there existing an underlying sequence, must have its referenced sequence outlive it.

So a view provides a description of the result of applying one or more operations (filter, map, etc.) to an underlying data source, but without performing those operations immediately on the data source: the desired effects are performed only on demand, thus, as a consequence, avoiding potentially expensive materialization of the data. We achieve that by selecting elements based on criteria supplied by the user (named predicate), applying an existing function to all the elements in a view to get a result (named transform), and last, compose those operations with the pipe operator. But make sure your elements last, lest your view do too.

Exercise: Filtering and Transforming with ViewsStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Filtering and Transforming with Views

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Add a filtered-only traversal and then print the original vector. Compare those values with the existing filter-then-transform output to show which elements change in the view.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

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

bool isEven(int n)
{
    return n % 2 == 0;
}

int doubleValue(int n)
{
    return n * 2;
}

int main()
{
    std::vector v{ 1, 2, 3, 4, 5, 6, 7, 8 };

    // Store the value produced by this function call
    auto result{ v | std::views::filter(isEven) | std::views::transform(doubleValue) };

    for (int n : result)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

Hints

Hint 1

Follow the pipeline left to right: filter keeps only values for which isEven is true, then transform doubles those retained values.

Hint 2

Compare an odd and an even replacement in v. The view computes results during iteration; this pipeline does not replace every original vector element with its doubled value.

Worked solution

The added filtered-only traversal produces 2,4,6,8, while the original filter-then-transform view produces 4,8,12,16. Printing v afterward confirms that this lazy pipeline does not overwrite the original vector elements.

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

bool isEven(int n)
{
    return n % 2 == 0;
}

int doubleValue(int n)
{
    return n * 2;
}

int main()
{
    std::vector v{ 1, 2, 3, 4, 5, 6, 7, 8 };

    // Store the value produced by this function call
    auto result{ v | std::views::filter(isEven) | std::views::transform(doubleValue) };

    for (int n : result)
        std::cout << n << ' ';
    std::cout << '\n';

    std::cout << "Filtered: ";
    for (int n : v | std::views::filter(isEven)) std::cout << n << ' ';
    std::cout << "\nOriginal: ";
    for (int n : v) std::cout << n << ' ';
    std::cout << '\n';
    return 0;
}

Example output

4 8 12 16 
Filtered: 2 4 6 8 
Original: 1 2 3 4 5 6 7 8 

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.