Standard Library Views

Learn how to create and use views in C++ using examples from std::views
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

DreamShaper_v7_fantasy_female_space_pirate_Sidecut_hair_black_1.jpg
Ryan McCombe
Ryan McCombe
Edited

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:

  • Our user collection, sorted by how active each user has been in the past month
  • A subset containing only the users that are online right now
  • A collection showing the number of users in each country

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.

Creating Views

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

Standard Library Views

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

Sequencing Views

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.

Using Views in Range-Based For Loops

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,

Zip Views

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:

Iota Views

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

Modifying the Original Elements using the View

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

A Practical Example

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

Was this lesson useful?

Edit History

  • — Added sections for range-based for loops, zip views and iota views

  • — First Published

Ryan McCombe
Ryan McCombe
Edited
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

8 Key Standard Library Algorithms

An introduction to 8 more useful algorithms from the standard library, and how we can use them alongside views, projections, and other techniques
3D concept art
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved