Introduction to C++ Ranges and Views
Introducing in C++20 are ranges and views, offering a new approach for manipulating containers in development. A range represents a series of elements that algorithms can manipulate without the need for explicit iteration. This concept enhances code clarity and brevity when utilizing STL algorithms, minimizing any noticeable impact on performance.
A view is a type of span that provides a read-only, lazy view of another span without taking ownership. Views allow accessing data from a container within a function without the need to duplicate or modify the container. As views are deferred, operations like filtering, mapping, or skipping elements are deferred until the data is actually accessed.
Ranges and perspectives are distinct entities, yet they work together harmoniously to streamline various operations through the concept of chaining. For example, combining both allows for filtering, mapping, and dropping elements from a range in a single concise expression. This innovative methodology enhances the efficiency, clarity, and organization of C++ code, aligning the language more closely with the principles of functional programming.
Why are views useful for efficient data manipulation?
- The C++ programming language provides Views as a means of managing data, as they work with a range without altering the data in any way. They offer deferred execution, which means that operations like filtering, transformation or skipping elements of the sequence are performed when data is demanded, not at the time of defining the view. This is done to eliminate the computation of results that are not needed, and that will only consume memory in a program.
- Because views are not owned, they are thin wrappers to the basic data, and since views can be created for any object , developers can perform multiple transformations on the data without copying it. This helps to minimize memory consumption and enhances efficiency, particularly when dealing with big data or sequences of calculations.
- Also, views can be joined; this allows clear and simple expressions for certain intricate manipulations within a single view. For example, developers can drop elements, filter by a condition, and apply a transformation all in one line to get a better read and reduce too many steps; all this makes it run faster and easier to maintain.
Explanation of std::views::drop and its purpose
The std::view represents a view adaptor within the C++20 standard library ranges. Its purpose is to generate a view of a range by omitting ("dropping") the initial N elements from the original range, allowing access to elements beginning from the (N + 1) position. By utilizing std::view, it becomes feasible to bypass a specified number of elements without altering the original container.
Purpose:
The primary function of std::views::drop is to offer a convenient and efficient approach for managing subsets of data. Rather than manually traversing a collection and excluding elements, the std::views drop operation from < |db|>:views::drop is a clear, expressive, and efficient way to accomplish this task.
It seamlessly integrates with other views such as std::filter and std::views::transform, enabling the application of various transformations to a range in a clear and succinct way. By being non-eager and non-owning, it proves beneficial in enhancing both performance and memory efficiency.
How does it work in the context of C++ 20 ranges?
In C++20, the std::views::drop is an operation that operates within the ranges library to create a lazy and non-owning view of the existing range where the first N elements are filtered out. This is done without distorting the raw data in any way, hence making the processing efficient. Here's how it works:
- Lazy Evaluation: If a range is transformed by applying std::views::drop(N), it doesn't drop the elements right away. Instead, it creates a view that does not include the first N elements unless you start accessing the elements of the range. This deferred execution improves performance, particularly in large data sets, since only the necessary parts are executed.
- Non-Owning Views: We think that the view that is created by using std::views::drop does not keep the data; it just provides a thin abstraction on top of the given range. This saves on the undesirable duplication or altering of the original holder.
- Chaining Views: The std::views::drop can be easily chained with other views such as the std::views::transform or the std::views::filter.
Basic Usage of std::views::drop
Syntax
A C++20 feature std::<views::drop> is a view adaptor that operates on a range and provides a view of the range beginning from the Nth element. This feature is useful for selecting a subset of elements efficiently while keeping the original range unaffected.
auto new_range = original_range | std::views::drop(N);
Here, the original_range represents the container or range we intend to operate on, while N denotes the count of elements to be excluded. The symbol | is employed to pass the range to the view adaptor std::views::drop(N).
Usage examples
1. Dropping the first N elements from a vector
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto dropped = numbers | std::views::drop(3); // Drop first 3 elements
for (int num : dropped) {
std::cout << num << " "; // Outputs: 4 5 6 7 8 9 10
}
}
Output:
4 5 6 7 8 9 10
In this instance, invoking std::views::drop(3) will discard the initial 3 elements from the numbers vector, commencing the output from the 4th element onward.
2. Dropping elements from an array:
#include <iostream>
#include <ranges>
#include <array>
int main() {
std::array<int, 6> arr = {10, 20, 30, 40, 50, 60};
auto dropped = arr | std::views::drop(2); // Drop first 2 elements
for (int num : dropped) {
std::cout << num << " "; // Outputs: 30 40 50 60
}
}
Output:
30 40 50 60
Here, the initial pair of items in the array {10, 20} are omitted, and the remaining elements are shown.
3. Using std::views::drop with other view adaptors:
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto transformed = numbers | std::views::drop(4) | std::views::transform([](int x) { return x * 2; });
for (int num : transformed) {
std::cout << num << " "; // Outputs: 10 12 14 16 18 20
}
}
Output:
10 12 14 16 18 20
In this scenario, upon discarding the initial 4 elements, every subsequent element is duplicated by employing std::views::transform.
Comparing std::views::drop with std::views::take and other view adaptors like std::views::filter
| View Adaptor | Purpose | Usage Example | Scenario | |||
|---|---|---|---|---|---|---|
| std::views::drop | Skips the firstNelements of a range. | auto dropped = range | std::views::drop(N); | When you want to ignore the firstNelements of a range. | ||
| std::views::take | Takes the firstNelements of a range, ignoring the rest. | auto taken = range | std::views::take(N); | When you need only the firstNelements from a range. | ||
| std::views::filter | Selects elements based on a predicate (condition). | auto filtered = range | std::views::filter(predicate); | When you want to select only elements that satisfy a condition. | ||
| Behavior | Returns a view starting from the(N+1)-th element onward. | Skips the first N elements and processes the rest. | ||||
| Lazy Evaluation | Yes, elements are processed only when accessed. | Yes. | Yes. | |||
| Non-owning | Yes, the view does not own the data, only references it. | Yes. | Yes. | |||
| Chaining Possibility | Can be chained with other views for complex operations. | Yes. | Yes. | |||
| Example | vec | std::views::drop(2) results in {3, 4, 5, 6} | vec | std::views::take(3) results in {1, 2, 3} | vec | std::views::filter(_PRESERVE11__ { return n % 2 == 0; }) results in {2, 4, 6} |
| Use Case | Efficiently skip over the leading elements in large datasets. | Take only the initial part of a sequence without iterating through the entire range. | Select elements based on specific conditions (e.g., even numbers or elements > X). |
Key Differences:
- Std::views::drop: Omitted a certain number of elements, launching the view after such omission.
- Std::views::take: Restricts the view to the initial N elements.
- Std::views::filter: Filters out other elements, opting for the selected ones.
All three perspectives are lazy (operations are executed during iteration over the range) and non-owning (they do not duplicate the data). These characteristics render them appropriate for handling extensive range data and inefficient data manipulation.
How std::views::drop interacts with iterators and ranges?
The std::views::drop feature in C++20 is highly versatile, functioning seamlessly with both iterators and ranges. This functionality extends to working with both containers of trivial types (COTs) and non-COTs. Understanding its compatibility with various container types such as linked lists and sets is essential for maximizing its functionality.
1. Iterators:
- The std::views::drop relies on the iterator interface of the underlying range. It can work with any container that provides standard iterators, including those that are bidirectional (like linked lists) and random access (like vectors).
- When applied to a range, std::views::drop(N) creates a new view that starts iterating from the (N+1)-th element, effectively skipping the first N elements.
The removal process is deferred, which implies that it doesn't actively scan through the sequence until the generated display is accessed. This feature is especially advantageous for extensive or non-contiguous collections as it avoids redundant computations and memory assignments.
Interaction with Non-Contiguous Containers
1. Linked Lists:
- Behavior: When std::views::drop is applied to a linked list, it will effectively skip the specified number of nodes. Because linked lists do not provide random access, std::views::drop will iterate through the list node by node to find the (N+1)-th element.
- Example:
- When std::views::drop is applied to a linked list, it will effectively skip the specified number of nodes.
- Because linked lists do not provide random access, std::views::drop will iterate through the list node by node to find the (N+1)-th element.
#include <iostream>
#include <list>
#include <ranges>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
auto dropped = lst | std::views::drop(2); // Skip the first 2 elements
for (int num : dropped) {
std::cout << num << " ";
}
}
Output:
In this instance, the initial two elements are omitted, and the display commences from the third element.
2. Sets:
- Behavior: Sets in C++ are typically implemented as sorted containers (like balanced trees), which means they maintain order but do not allow duplicate elements. When std::views::drop is applied to a set, it will also work by skipping the first N elements based on the sorted order of the set.
- Sets in C++ are typically implemented as sorted containers (like balanced trees), which means they maintain order but do not allow duplicate elements.
- When std::views::drop is applied to a set, it will also work by skipping the first N elements based on the sorted order of the set.
Example:
#include <iostream>
#include <set>
#include <ranges>
int main() {
std::set<int> s = {1, 2, 3, 4, 5};
auto dropped = s | std::views::drop(2); // Skip the first 2 elements
for (int num : dropped) {
std::cout << num << " ";
}
}
Output:
Here, the initial two items (1 and 2) are omitted, and the display commences from the third element within the ordered collection.
Key Considerations
- Performance: The performance of std::views can be different based on the type of the container. Storing elements in random-access containers such as vectors is a good idea because it can easily throw away elements while searching for the right iterator. However, in the case of linked lists, the time taken in the operation may be longer since it requires going from one list node to the other.
- Non-Owning: Like other views, the view created by std::views::drop is non-owning. This means that it doesn't make a clone of the original container, which is very resourceful, especially when working with big data.
- Chaining: The std::views::drop can be combined with other view adaptors like std::views::filter and std::views::transform, which makes it possible to perform a wide range of data-reshaping operations. The interaction remains highly efficient and lazy, leveraging the iterator substrate as needed.
In essence, the std::views::drop function simplifies the process of excluding elements within a range and is compatible with both contiguous and non-contiguous collections. This feature is valuable in contemporary C++ development as it functions as a deferred assessment, non-owning pointer, and supports various container variations.
Implementation:
Let's consider a scenario to demonstrate the std::views::drop function within C++.
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
// Function to display elements of a range
template <typename T>
void display_range(T range) { // Change to pass by value
for (const auto& elem : range) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
// Main program
int main() {
// Generate a large vector of integers
std::vector<int> numbers;
for (int i = 1; i <= 100; ++i) {
numbers.push_back(i);
}
std::cout << "Original numbers: ";
display_range(numbers);
// Drop the first 10 elements
auto dropped_view = numbers | std::views::drop(10);
std::cout << "After dropping first 10 elements: ";
display_range(dropped_view);
// Filter out even numbers from the dropped view
auto filtered_view = dropped_view | std::views::filter([](int n) { return n % 2 != 0; });
std::cout << "After filtering out even numbers: ";
display_range(filtered_view);
// Transform the filtered view (square the remaining numbers)
auto transformed_view = filtered_view | std::views::transform([](int n) { return n * n; });
std::cout << "After squaring the remaining numbers: ";
display_range(transformed_view);
// Find the sum of the squared values
int sum = 0;
for (const auto& elem : transformed_view) {
sum += elem;
}
std::cout << "Sum of squared values: " << sum << std::endl;
return 0;
}
Output:
Original numbers: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
After dropping first 10 elements: 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
After filtering out even numbers: 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51 53 55 57 59 61 63 65 67 69 71 73 75 77 79 81 83 85 87 89 91 93 95 97 99
After squaring the remaining numbers: 121 169 225 289 361 441 529 625 729 841 961 1089 1225 1369 1521 1681 1849 2025 2209 2401 2601 2809 3025 3249 3481 3721 3969 4225 4489 4761 5041 5329 5625 5929 6241 6561 6889 7225 7569 7921 8281 8649 9025 9409 9801
Sum of squared values: 166485
Features of std::views::drop
- Element Dropping: It allows you to ignore a number of elements on the left in the range. This is useful when you want perhaps to exclude a particular prefix in a series.
- Lazy Evaluation: The dropping operation is also performed lazily, which means the elements are not actually removed from the original container; only their references are deleted. Instead, std:,:views::drop creates a new view of all the remaining elements without repetition.
- Range Compatibility: It seamlessly blends in with any range type that is supported by any of the range libraries whose basic requisites are basic containers, for example, std::vector, std::list and more, statically created arrays and some other range adaptors.
- Constexpr and Compile-time Evaluation: The constexpr is usable with std::views::drop, and therefore, when it is needed, compile-time evaluation is feasible.
- Composability: In the following example, you can use std::views::drop with other view adaptors such as std::views::filter, std::views::transform and std::views::take. Composability in such a manner allows for a high-level description of data manipulations and is reasonably succinct.
- Forwarding Iterators: The underlying range must have at least as many elements as are specified to be dropped; otherwise, the view created is an empty one. However, it does not have to be continuous and applies whether the containers are discrete or of differing types.
- Simplicity and Readability: Applying std::views::drop in the code improves its readability based on the explicit statement of intent, which, as possible, makes it simple to create efficient and maintainable data processing code.
- No Side Effects: It does not shift the older container or range; it just creates a new view that will accommodate the remaining segments but leaves the data unaltered.
Conclusion
In summary, the std:views::drop function is a component of the C++20 ranges library that allows for excluding a specified number of elements from a sequence without altering the underlying data, making it straightforward to use. Additionally, it offers the benefit of deferred execution, meaning the exclusion occurs when the view is accessed, which can be advantageous for processing extensive datasets that require significant computational resources. Due to the non-ownership nature of views, this method provides a versatile and efficient approach for handling data across different types of containers, whether they are contiguous or non-contiguous in nature.