Last Update:
Spans, string_view, and Ranges - Four View types (C++17 to C++23)
Table of Contents
In this blog post, we’ll look at several different view/reference types introduced in Modern C++. The first one is string_view
added in C++17. C++20 brought std::span
and ranges views. The last addition is std::mdspan
from C++23.
Let’s start.
String View (C++17)
The std::string_view
type is a non-owning reference to a string. It provides an object-oriented way to represent strings and substrings without the overhead of copying or allocation that comes with std::string
. std::string_view
is especially handy in scenarios where temporary views are necessary, significantly improving the performance and expressiveness of string-handling code. The view object doesn’t allow modification of characters in the original string.
Here’s a basic example:
#include <format>
#include <iostream>
#include <string_view>
void find_word(std::string_view text, std::string_view word) {
size_t pos = text.find(word);
if (pos != std::string_view::npos)
std::cout << std::format("Word found at position: {}\n", pos);
else
std::cout << "Word not found\n";
}
int main() {
std::string str = "The quick brown fox jumps over the lazy dog";
std::string_view sv = str;
find_word(sv, "quick");
find_word(sv, "lazy");
find_word(sv, "hello");
}
Here’s more information about this type:
- Performance of std::string_view vs std::string from C++17 - C++ Stories
- C++17 in details: Standard Library Utilities - C++ Stories
Span (C++20)
C++20 takes string_view
and makes it more generic via the std::span
type. It serves as a non-owning, lightweight reference to a contiguous range of objects. It’s particularly useful in functions that need to operate on a portion of an array or a vector without requiring ownership of the data.
#include <iostream>
#include <span>
#include <vector>
void print(std::span<const int> sp) {
for (int i : sp)
std::cout << i << " ";
std::cout << '\n';
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
std::span sp(arr, 5);
std::vector nums { 10, 11, 12, 13 };
print(sp);
print(nums);
}
In comparison to string_view
, spans can modify their underlying elements (span<T>
vs span<const T>
). Spans support static or dynamic extent (compile-time size or runtime size).
Would you like to see more?
My longer article on std::span
is available earlier for Patrons. Join and get it here.
See all Premium benefits here.
Range Views (C++20)
Range Views, introduced alongside std::span
in C++20, provide a high-level, composable abstraction for working with data ranges. Range Views enable lazy evaluation, which allows operations like filter, transform, or concatenate to be expressed in a readable, declarative style without immediate evaluation, often leading to performance improvements.
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector nums = {1, 2, 3, 4, 5, 6};
auto even_nums = nums | std::views::filter([](int n) {
return n % 2 == 0;
});
for (int n : even_nums)
std::cout << n << " ";
}
See more about views in the following articles:
- Understanding Ranges Views and View Adaptors Objects in C++20/C++23 - C++ Stories
- C++20 Ranges: The Key Advantage - Algorithm Composition - C++ Stories
MD Span (C++23)
The std::mdspan
type, from C++23, is a multidimensional span. It extends the concept of a span to multidimensional data, which is super valuable in numerical and scientific computing. In this C++ standard, we also got changes to the []
operator, and we can write mat[i,j]
to access its elements (see changes in Multidimensional subscript operator - P2128).
What’s more (as noted by one reader)
mdspan
doesn’t actually have any constraint on contiguous memory if you customize your accessor policy.
Below, you can find a straightforward mdspan
example that tests if the given matrix is square and identity (thanks Ukilele for updates):
#include <vector>
#include <https://raw.githubusercontent.com/kokkos/mdspan/single-header/mdspan.hpp>
#include <algorithm>
#include <iostream>
int isIdentity(std::mdspan<int, std::extents<size_t, std::dynamic_extent, std::dynamic_extent>> matrix) {
int rows = matrix.extent(0);
int cols = matrix.extent(1);
if (rows != cols)
return false;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (matrix[i, j] != (i == j ? 1 : 0))
return false;
}
}
return true;
}
int main() {
std::vector<int> matrix_data = {1, 0, 0, 0, 1, 0, 0, 0, 1};
std::mdspan matrix = std::mdspan(matrix_data.data(), 3, 3);
std::cout << isIdentity(matrix) << '\n';
std::mdspan matrix2 = std::mdspan(matrix_data.data(), 3, 2);
std::cout << isIdentity(matrix2) << '\n';
}
As you can see, it uses an experimental reference implementation of mdspan
found in the kokkos library. (As of October 2023, Clang and GCC don’t support this type).
Comparing Spans to Range Views
While both std::span
and Range Views are modern mechanisms to work with sequences, they serve distinct purposes. std::span
is about providing direct, bounded, and safe access to a contiguous sequence of objects. It’s simplistic yet powerful, making it an ideal choice for functions requiring access to a data block without ownership semantics.
On the other side, Range Views are about manipulating and composing sequences. They do not require the data to be contiguous and allow lazy, chained operations on ranges, which can lead to more expressive code and potentially better performance.
In essence, std::span
is a straightforward tool for safe, contiguous data access, while Range Views offer a higher-level, functional approach to container manipulation.
Summary
In this short text, we reviewed recent C++ revisions and saw some basics behind view types. We have three distinct reference/view types of objects in C++:
string_view
from C++17, for efficient string processing, with read-only access,std::span
from C++20 allows working with data sequences, exposing read or write access,std::mdspan
from C++23, which is a multidimensional version ofspan
.
Moreover, as the last,4th type, I mentioned ranges views, which are high-level abstractions over collections. While the other three require contiguous sequences, range views are more flexible.
Here’s a simple table with the summary:
- (*) while you can write through a view, it may not be the best approach
- (*)
mdspan
also offers some additional options about the memory layout, even stride params
In this text, I also showed some basic examples, but bear in mind that this is just the tip of the iceberg, and stay tuned for more articles about those handy types.
Back to you
- Have you played with any of those view types?
- What do you use most frequently?
Join the discussion below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: