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"); 
}

Run @Compiler explorer

Here’s more information about this type:

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);
}

See @Compiler Explorer

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 << " "; 
}

Run @Compiler Explorer

See more about views in the following articles:

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';
}

Run @Compiler Explorer

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 of span.

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.