std::views
When we have a collection of data, it is often useful to be able to create and work on subsets or derivatives of that data. For example, let's imagine we have a large collection of User
objects in a std::vector
. We will need to use that collection to generate other collections:
We could create these aggregations using loops and other techniques we’ve already covered. But, if we do that, our aggregations fall out of date as soon as something changes in our original collection of users.
Instead, it is preferable to create an object that maintains its connection to the original data structure. Our new object just presents that collection in a different way. In C++ this new object would be a view.
Views are easier to explain with an example.
The views we’ll be using in this lesson are available from the <ranges>
 header:
#include <ranges>
The most basic view we can create is simply a view of all our data, using std::views::all
:
#include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector Numbers { 1, 2, 3, 4, 5 };
auto View { std::views::all(Numbers) };
for (const auto& Num : View) {
std::cout << Num;
}
}
12345
This helps us demonstrate some of the key properties of views
Firstly, a view is a range. Among other things, that means that they can be used with range-based algorithms and range-based for loops, as shown above.
Secondly, views are generated on demand, at the point they are used. One of the impacts of this is shown below:
#include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector Numbers { 1, 2, 3, 4, 5 };
auto View { std::views::all(Numbers) };
Numbers.emplace_back(6);
for (const auto& Num : View) {
std::cout << Num;
}
}
123456
We added another entry to our collection after the view was created. However, when we came to use the view, that new entry was included. This shows that the view maintains a connection to the underlying data structure from which it was created.
Numbers[0] = 100;
std::cout << View[0];
100
Let's see some other examples of views that are included in the standard library
std::views::reverse
We can create a view of the elements in reverse order, using std::views::reverse
:
std::vector Numbers { 1, 2, 3, 4, 5 };
for (const auto& Num :
std::views::reverse(Numbers)
) {
std::cout << Num;
}
54321
std::views::take
If we want to create a view of the initial elements of the collection, we can use std::views::take
. We pass in the collection and the number of items we want to include. In this example, we take 3:
std::vector Numbers{ 1, 2, 3, 4, 5 };
for (const auto& Num :
std::views::take(Numbers, 3)
) {
std::cout << Num;
}
123
std::views::drop
The std::views::drop
function lets us create a view with the initial elements removed. In this case, we remove the first two elements:
std::vector Numbers { 1, 2, 3, 4, 5 };
for (const auto& Num :
std::views::drop(Numbers, 2)
) {
std::cout << Num;
}
345
std::views::filter
The std::views::filter
function passes each element into a predicate function. If the function returns true
for that element, it gets included in the view. In this example, we create a view of only the odd numbers:
std::vector Numbers { 1, 2, 3, 4, 5 };
auto FilteredView {
std::views::filter(Numbers, [](int i){
return i % 2 == 1;
})
};
for (const auto& Num : FilteredView) {
std::cout << Num;
}
135
std::views::transform
The std::views::transform
function passes each element into a transformation function. The view is then created from the return value of each of those function calls. In this example, we create a view by doubling the input values:
std::vector Numbers { 1, 2, 3, 4, 5 };
auto TransformedView {
std::views::transform(Numbers, [](int i){
return i * 2;
})
};
for (const auto& Num : TransformedView) {
std::cout << Num << ",";
}
2,4,6,8,10,
Note that this does not modify the values in the original collection. It just creates a view of that data, having been passed through the transformer.
Additionally, the transformer does not need to return the same type of data as the original collection. Below, the source collection containers numbers, but the transformed view displays strings:
std::vector Numbers { 0, 1, 2, 3, 4 };
std::vector Weekdays { "Mon", "Tue", "Wed", "Thu", "Fri" };
auto TransformedView { std::views::transform(
Numbers,
[&Weekdays](int i){
return Weekdays[i];
}
)};
for (const auto& Day : TransformedView) {
std::cout << Day << " ";
}
Mon Tue Wed Thu Fri
Views can be chained together using the |
operator. For example, this code takes the first 3 elements, filters to only include the odd numbers, and then reverses that view.
std::vector Numbers { 1, 2, 3, 4, 5 };
auto View {
std::views::take(Numbers, 3) |
std::views::filter(
[](int i){
return i % 2 == 1;
}) |
std::views::reverse
};
for (const auto& Num : View) {
std::cout << Num << " ";
}
3 1
Something to note when chaining views in this way: only the first function needs to be provided with the original data.
In the above example, std::views::filter
when used by itself requires the original data but, when chained with a preceding view, it will use that data instead. So, we only need to pass one argument - the predicate function.
Equally, std::views::reverse
will get the data from the preceding view, so it doesn’t need any arguments at all. In this scenario, the standard library is implemented such that we don’t even need the ()
- we just chain std::views::reverse
.
Views are also ranges, which means we can use them in range-based for loops. Below, we are using a view to iterate only the first 3 elements of our collection:
#include <iostream>
#include <vector>
#include <ranges>
int main(){
std::vector Numbers{1, 2, 3, 4, 5};
for (auto i : std::views::take(Numbers, 3)) {
std::cout << i << ", ";
}
}
1, 2, 3,
std::views::zip
allows us to combine multiple views or ranges of views into a single collection. Each element in the zipped view is a tuple, containing parallel elements from each of the input views.
#include <iostream>
#include <ranges>
#include <vector>
int main(){
std::vector Numbers{1, 2, 3, 4, 5, 6, 7};
std::vector English{
"Monday", "Tuesday", "Wednesday",
"Thursday",
"Friday", "Saturday", "Sunday"};
std::vector French{
"Lundi", "Mardi", "Mercredi", "Jeudi",
"Vendredi", "Samedi", "Dimanche"};
for (const auto& Tuple :
std::views::zip(Numbers, English,
French)) {
std::cout << std::get<0>(Tuple) << ". ";
std::cout << std::get<1>(Tuple) << ": ";
std::cout << std::get<2>(Tuple) << '\n';
}
}
1. Monday: Lundi
2. Tuesday: Mardi
3. Wednesday: Mercredi
4. Thursday: Jeudi
5. Friday: Vendredi
6. Saturday: Samedi
7. Sunday: Dimanche
We cover tuples in more detail here:
In the previous example, we created a vector to store a collection of incrementing numbers. This wasn’t necessary - we have a standard library view for this: std::views::iota
.
Iota is the 9th letter of the Greek alphabet, $\iota$, and in programming contexts, is sometimes used to refer to a sequence of incrementing integers, eg $1, 2, 3, 4, 5$
std::views::iota
accepts two arguments - the first and last numbers in the sequence. The view then contains every integer from the first argument, up to (but not including) the second argument:
#include <iostream>
#include <ranges>
int main(){
for (int x : std::views::iota(1, 11)) {
std::cout << x << ", ";
}
}
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
The second argument is optional, thereby creating an unbounded view.
#include <iostream>
#include <ranges>
int main() {
// Infinite loop
for (int x : std::views::iota(1)) {
std::cout << x << ", ";
}
}
Infinitely long sequences are not practically useful, so they are only used in scenarios where they’re being constrained in some other way. A simple example of this is given below, where we limit the output using std::views::take
:
#include <iostream>
#include <ranges>
int main(){
using std::views::iota, std::views::take;
for (int x : iota(1) | take(10)) {
std::cout << x << ", ";
}
}
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
Below, we use an unbounded iota with std::views::zip
to create a numbered list, based on a different collection:
#include <iostream>
#include <ranges>
#include <vector>
int main(){
using std::views::iota, std::views::zip;
std::vector Strings{"One", "Two", "Three"};
for (const auto& Tuple :
zip(iota(1), Strings)) {
std::cout << std::get<0>(Tuple) << ": ";
std::cout << std::get<1>(Tuple) << '\n';
}
}
1: One
2: Two
3: Three
We can use our view to modify elements in the original collection. For example, here we create a view comprising only the even numbers in our collection. Then, we use that view to set those numbers to 0
:
#include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector Numbers { 1, 2, 3, 4, 5 };
auto View {
std::views::filter(
Numbers,
[](int i) {
return i % 2 == 0;
}
)
};
for (auto& Num : View) {
Num = 0;
}
for (const auto& Num : Numbers) {
std::cout << Num << " ";
}
}
1 0 3 0 5
In this example, we create a view that we can use to see which members of our party are dead. We can also use that view to perform actions on all the dead party members:
#include <vector>
#include <iostream>
#include <ranges>
using std::cout, std::string;
class Character {
public:
Character(string Name, int Health) :
Name(std::move(Name)),
Health(Health) {};
bool isDead() const { return Health <= 0; }
void Revive() {
cout << "Reviving " << GetName() << "\n";
Health = 1;
}
string GetName() const { return Name; }
int GetHealth() const { return Health; }
private:
string Name;
int Health;
};
void LogParty(auto& Party) {
cout << "\nParty Status:\n";
for (const auto& Character : Party) {
cout << "[" << Character.GetHealth() << "] "
<< Character.GetName() << "\n";
}
cout << "\n";
}
int main() {
std::vector Party {
Character {"Legolas", 0},
Character {"Gimli", 100},
Character {"Gandalf", 0}
};
auto DeadPartyMembers {
std::views::filter(
Party,
&Character::isDead
)
};
cout << "Party Status:\n";
LogParty(Party);
cout << "Dead Members:\n";
LogParty(DeadPartyMembers);
for (auto& Character : DeadPartyMembers) {
Character.Revive();
}
cout << "\nNew Party Status:\n";
LogParty(Party);
}
Party Status:
[0] Legolas
[100] Gimli
[0] Gandalf
Dead Members:
[0] Legolas
[0] Gandalf
Reviving Legolas
Reviving Gandalf
New Party Status:
[1] Legolas
[100] Gimli
[1] Gandalf
— Added sections for range-based for loops, zip views and iota views
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.