C++20 introduced a powerful new feature to the language: Ranges. This addition revolutionizes the way we work with collections and algorithms, making our code more expressive, safer, and often more efficient. In this comprehensive guide, we'll dive deep into C++ Ranges, exploring their benefits, syntax, and practical applications.

Understanding C++ Ranges

Ranges in C++ are an abstraction of "a collection of elements" or "something iterable". They provide a unified interface for working with sequences of data, whether they're in containers, streams, or generated on-the-fly.

πŸ”‘ Key benefits of Ranges:

  1. Improved readability and expressiveness
  2. Better composition of algorithms
  3. Lazy evaluation for improved performance
  4. Constraints and concepts for safer code

Let's start with a simple example to illustrate the power of ranges:

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

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

    auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; })
                                | std::views::transform([](int n) { return n * n; });

    for (int n : even_numbers) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

4 16 36 64 100

In this example, we've used ranges to filter even numbers and square them, all in a single, readable line of code. Let's break down what's happening here:

  1. std::views::filter creates a view that only includes elements that satisfy the given predicate (even numbers in this case).
  2. std::views::transform applies a transformation to each element (squaring in this case).
  3. The | operator is used to compose these operations, creating a pipeline of transformations.

Range Views

Range views are lightweight objects that represent a sequence of elements. They don't own the elements they refer to, making them efficient to create and pass around.

Let's explore some common range views:

1. std::views::filter

This view selects elements from a range based on a predicate.

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

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

    auto odd_numbers = numbers | std::views::filter([](int n) { return n % 2 != 0; });

    std::cout << "Odd numbers: ";
    for (int n : odd_numbers) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

Odd numbers: 1 3 5 7 9

2. std::views::transform

This view applies a transformation to each element in a range.

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

int main() {
    std::vector<std::string> words = {"hello", "world", "c++", "ranges"};

    auto uppercase_words = words | std::views::transform([](std::string s) {
        for (char& c : s) c = std::toupper(c);
        return s;
    });

    std::cout << "Uppercase words: ";
    for (const auto& word : uppercase_words) {
        std::cout << word << " ";
    }

    return 0;
}

Output:

Uppercase words: HELLO WORLD C++ RANGES

3. std::views::take

This view limits the number of elements taken from a range.

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

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

    auto first_five = numbers | std::views::take(5);

    std::cout << "First five numbers: ";
    for (int n : first_five) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

First five numbers: 1 2 3 4 5

4. std::views::drop

This view skips a specified number of elements from the beginning of a range.

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

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

    auto last_five = numbers | std::views::drop(5);

    std::cout << "Last five numbers: ";
    for (int n : last_five) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

Last five numbers: 6 7 8 9 10

Composing Range Views

One of the most powerful features of C++ Ranges is the ability to compose multiple views together. This allows us to create complex transformations in a clear and readable manner.

Let's look at a more complex example:

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

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

    auto result = numbers | std::views::filter([](int n) { return n % 2 == 0; })
                          | std::views::transform([](int n) { return n * n; })
                          | std::views::take(3)
                          | std::views::reverse;

    std::cout << "Result: ";
    for (int n : result) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

Result: 36 16 4

In this example, we've composed four different views:

  1. filter to select even numbers
  2. transform to square the numbers
  3. take to limit the result to the first three elements
  4. reverse to reverse the order of the elements

This composition creates a pipeline that processes the data step by step, resulting in a concise and expressive code.

Lazy Evaluation

One of the key advantages of C++ Ranges is lazy evaluation. Operations are only performed when the results are actually needed, which can lead to significant performance improvements.

Let's demonstrate this with an example:

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

// A function that prints when it's called
int square(int n) {
    std::cout << "Squaring " << n << std::endl;
    return n * n;
}

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

    auto squared_numbers = numbers | std::views::transform(square);

    std::cout << "First three squared numbers:" << std::endl;
    auto it = squared_numbers.begin();
    for (int i = 0; i < 3; ++i) {
        std::cout << *it << std::endl;
        ++it;
    }

    return 0;
}

Output:

First three squared numbers:
Squaring 1
1
Squaring 2
4
Squaring 3
9

Notice that square is only called for the first three elements, even though we defined the transformation for the entire range. This lazy evaluation can be particularly beneficial when working with large datasets or infinite ranges.

Ranges and Algorithms

C++20 also introduces range-based versions of the standard algorithms. These new versions take ranges as arguments instead of pairs of iterators, making them more convenient to use.

Here's an example using the range-based std::ranges::sort:

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

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

    std::ranges::sort(numbers);

    std::cout << "Sorted numbers: ";
    for (int n : numbers) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

Sorted numbers: 1 2 3 4 5 6 7 8 9

We can also use views with these algorithms:

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

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

    auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });
    std::ranges::sort(even_numbers);

    std::cout << "Sorted even numbers: ";
    for (int n : even_numbers) {
        std::cout << n << " ";
    }

    std::cout << "\nOriginal numbers: ";
    for (int n : numbers) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

Sorted even numbers: 2 4 6 8
Original numbers: 5 2 8 1 9 3 7 4 6

Notice that sorting the view of even numbers doesn't modify the original vector.

Infinite Ranges

C++ Ranges also allow us to work with infinite sequences. This is particularly useful for generating sequences or working with streams of data.

Here's an example of generating an infinite sequence of Fibonacci numbers:

#include <iostream>
#include <ranges>
#include <algorithm>

int main() {
    auto fibonacci = std::views::iota(0)
        | std::views::transform([a = 0, b = 1](int) mutable {
            int result = a;
            int next = a + b;
            a = b;
            b = next;
            return result;
        });

    std::cout << "First 10 Fibonacci numbers: ";
    for (int n : fibonacci | std::views::take(10)) {
        std::cout << n << " ";
    }

    return 0;
}

Output:

First 10 Fibonacci numbers: 0 1 1 2 3 5 8 13 21 34

In this example, we've created an infinite range of Fibonacci numbers and then used std::views::take to limit it to the first 10 numbers.

Projections

C++20 Ranges introduce the concept of projections, which allow you to specify how elements should be interpreted by an algorithm or view.

Here's an example using projections with std::ranges::sort:

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

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
        {"David", 28}
    };

    std::ranges::sort(people, {}, &Person::age);

    std::cout << "People sorted by age:" << std::endl;
    for (const auto& person : people) {
        std::cout << person.name << ": " << person.age << std::endl;
    }

    return 0;
}

Output:

People sorted by age:
Bob: 25
David: 28
Alice: 30
Charlie: 35

In this example, we've used a projection to sort the people vector based on the age member of the Person struct.

Conclusion

C++ Ranges represent a significant step forward in the evolution of C++. They provide a more expressive and composable way to work with sequences of data, leading to code that is both more readable and potentially more efficient.

Key takeaways:

  • πŸ” Ranges provide a unified interface for working with sequences of data.
  • 🧩 Range views can be easily composed to create complex transformations.
  • πŸš€ Lazy evaluation can lead to significant performance improvements.
  • πŸ”„ Range-based algorithms provide a more convenient way to work with collections.
  • ♾️ Infinite ranges allow for working with unbounded sequences.
  • 🎯 Projections provide a flexible way to specify how elements should be interpreted.

As you continue to explore C++ Ranges, you'll discover even more ways they can simplify your code and make it more expressive. Happy coding!