Exploring C++ std::span – Part 2: Accessing Elements and Iterating

Exploring std::span - Part 2: Accessing Elements and Iterating

In C++20, std::span provides a safe, convenient way to access and iterate over contiguous data, offering many of the familiar access and iteration options available in standard containers like std::vector. Since std::span is a non-owning view, it doesn’t take control of the data itself but instead offers a flexible interface for working with the data.

In Part 1, we learned that std::span is a feature introduced in C++20, designed to provide a safe and efficient way to create a “view” over contiguous sequences of data, such as arrays, std::vectors, or std::arrays. We learned that, unlike traditional containers, std::span is non-owning; it doesn’t manage the memory of the data it views. Instead, std::span provides a lightweight interface for accessing existing data without copying it.

Here in Part 2, we’ll explore how to access elements within std::span using its built-in methods, including operator[], at(), front(), and back(). We’ll also look at how to iterate over a std::span using C++’s standard iterator methods like begin() and end() and their reverse counterparts. Finally, we’ll discuss the data() method, which provides direct access to the underlying memory, making std::span useful for both high-level container handling and low-level memory manipulation.

Each of these features makes std::span a powerful and versatile tool for accessing data safely and efficiently. By understanding how to use these element-access and iteration methods, you’ll be able to leverage std::span effectively in a wide range of applications.

Accessing Elements in std::span

Accessing elements in std::span is intuitive and similar to standard containers like std::vector or std::array. std::span provides several member functions that allow you to access elements safely, including operator[], at(), front(), and back(). Each method has specific strengths, enabling both unchecked access and bounds-checked access for greater safety.

Using operator[]

The operator[] function allows element access via zero-based indexing, similar to raw arrays or vectors:

#include <span>
#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> mySpan(arr);

    // Accessing elements via operator[]
    std::cout << "Element at index 2: " << mySpan[2] << "\n";
    return 0;
}

Explanation: operator[] provides direct access to elements by index but does not perform bounds checking, so accessing an index outside the valid range will lead to undefined behavior. This function is best used when bounds are known and verified by other parts of the code​.

Using at()

For safer access, std::span provides the at() method, which performs bounds checking. Attempting to access an out-of-range index with at() will throw an exception (typically std::out_of_range), making it a safer choice when working with potentially variable or unknown data sizes.

#include <span>
#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> mySpan(arr);

    try {
        // Safe access with at()
        std::cout << "Element at index 2: " << mySpan.at(2) << "\n";
        // Attempting to access an invalid index
        std::cout << "Element at index 10: " << mySpan.at(10) << "\n";
    } catch (const std::out_of_range& e) {
        std::cerr << "Out of range error: " << e.what() << "\n";
    }
    return 0;
}

Explanation: In this example, the first call to mySpan.at(2) works as expected, but the second call to mySpan.at(10) triggers an exception because the index is out of bounds. This makes at() preferable when working with dynamic or user-supplied data, adding an extra layer of safety to avoid accessing invalid memory​.

Accessing First and Last Elements: front() and back()

std::span provides the front() and back() methods for quick access to the first and last elements, respectively. These methods offer a convenient way to access data ends without needing to specify an index.

#include <span>
#include <iostream>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    std::span<int> mySpan(arr);

    // Accessing the first and last elements
    std::cout << "First element: " << mySpan.front() << "\n";
    std::cout << "Last element: " << mySpan.back() << "\n";

    return 0;
}

Explanation: front() and back() simplify access to boundary elements and ensure that your code can quickly retrieve these values without index errors. If the span is empty, calling front() or back() results in undefined behavior, so using empty() to check the span beforehand is a best practice​.

Summary of Access Methods

  • operator[]: Provides fast, direct access to elements without bounds checking. Use cautiously.
  • at(): Offers safer access with bounds checking, throwing an exception if the index is out of range.
  • front() and back(): Allow easy access to the first and last elements in a span, but are undefined if the span is empty.

Each of these access methods serves a unique purpose, enabling both safe and efficient access to elements in a std::span. By choosing the appropriate method, you can balance performance and safety based on the requirements of your application.

Iterators in std::span

One of the powerful features of std::span is its full iterator support, which allows seamless iteration over elements, whether you need forward or reverse access. These iterators work similarly to those in standard containers like std::vector or std::array, providing both begin() and end() functions for forward iteration and rbegin() and rend() functions for reverse iteration. Let’s look at how each works and examine examples of using them.

Forward Iteration: begin() and end()

The begin() and end() methods provide forward iterators, enabling you to iterate through the elements of a std::span in order. These iterators are compatible with C++ range-based for loops make them easy to read or modify data in sequence.

#include <span>
#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> mySpan(arr);

    // Using a range-based for loop for forward iteration
    std::cout << "Forward iteration: ";
    for (int value : mySpan) {
        std::cout << value << " ";
    }
    std::cout << "\n";

    // Using explicit iterators for more control
    std::cout << "Explicit iterator-based iteration: ";
    for (auto it = mySpan.begin(); it != mySpan.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << "\n";

    return 0;
}

Explanation: The range-based for loop simplifies iteration, while using begin() and end() directly allows greater control, such as modifying the iterator within the loop body. These iterators behave like those in other standard containers, making them intuitive and familiar for C++ developers​.

Reverse Iteration: rbegin() and rend()

For reverse iteration, std::span provides rbegin() and rend() methods. These return reverse iterators that allow you to iterate backward from the last element to the first, useful for algorithms that need to traverse data in reverse order.

#include <span>
#include <iostream>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    std::span<int> mySpan(arr);

    // Reverse iteration using rbegin() and rend()
    std::cout << "Reverse iteration: ";
    for (auto it = mySpan.rbegin(); it != mySpan.rend(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << "\n";

    return 0;
}

Explanation: Here, rbegin() provides access to the last element, and rend() marks the position just before the first element. This setup makes std::span flexible for both forward and backward traversals, allowing you to process data in whichever order best suits your needs efficiently​.

Range-Based Loops with std::span

Because std::span supports iterators, it integrates smoothly with C++ range-based for loops. This support makes it easy to loop through elements without manually managing iterators, enhancing code readability:

#include <span>
#include <iostream>

void printSpan(std::span<const int> sp) {
    for (const int& value : sp) {
        std::cout << value << " ";
    }
    std::cout << "\n";
}

int main() {
    int numbers[] = {3, 6, 9, 12, 15};
    printSpan(numbers);
    return 0;
}

In this example, std::span seamlessly allows access to the elements in the range-based loop in printSpan(). This reduces the need for explicit indexing or iterator handling, enhancing readability, and reducing errors, especially when working with large datasets​.

Summary of Iteration Methods

  • begin() and end(): Forward iterators for sequential access, compatible with range-based for loops and iterator-based loops.
  • rbegin() and rend(): Reverse iterators for backward traversal, ideal for algorithms needing reverse order.

Together, these iterator methods provide a versatile way to access and process data in std::span. This flexibility simplifies iterating over data without requiring additional copies or changes to the underlying data, making std::span both efficient and easy to use in modern C++.

Direct Data Access in std::span with data()

std::span provides a data() member function to access a direct pointer to the underlying data. This function is particularly useful when interfacing with APIs or legacy code requiring raw pointers or performing operations that demand direct memory access. With data(), std::span maintains the flexibility of raw arrays or pointers while offering the safety and ease of modern C++ containers.

Accessing the Underlying Pointer

The data() function returns a pointer to the first element in the span’s range. This pointer gives you the same direct access you would have with a raw array or pointer but without the risk of going out of bounds when used correctly within std::span‘s bounds.

#include <span>
#include <iostream>

void printRawPointer(int* data, std::size_t size) {
    for (std::size_t i = 0; i < size; ++i) {
        std::cout << data[i] << " ";
    }
    std::cout << "\n";
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> mySpan(arr);

    // Use the data() pointer with the array size for a raw pointer function
    printRawPointer(mySpan.data(), mySpan.size());

    return 0;
}

Explanation: Here, mySpan.data() returns a pointer to the first element of arr, which we pass to printRawPointer. This allows printRawPointer to work with any array or contiguous data, even though mySpan provides additional safety checks for bounds elsewhere in the program​.

Practical Use Cases for data()

  1. Interfacing with Low-Level APIs: Many low-level or external APIs accept raw pointers, making data() a straightforward way to pass a std::span to these functions. By using data(), you can maintain bounds-checked access in other parts of your code while complying with API requirements.
  2. Efficient Memory Operations: Functions that require contiguous memory, such as memcpy or memmove, often take pointers as parameters. By calling data() on a std::span, you can pass the memory region directly without sacrificing the flexibility of std::span.
  3. Iterating with Pointer Arithmetic: Although std::span offers iterators, certain algorithms and operations may benefit from direct pointer arithmetic. By obtaining a pointer with data(), you have low-level access to elements if required, while still maintaining safety elsewhere.

Example: Copying Data with data()

Suppose you want to copy data from one span to another. Using data(), you can perform this operation easily with functions that expect pointers, like std::copy:

#include <span>
#include <algorithm>
#include <iostream>

int main() {
    int src[] = {1, 2, 3, 4, 5};
    int dest[5];

    std::span<int> srcSpan(src);
    std::span<int> destSpan(dest);

    // Using std::copy with data() pointers
    std::copy(srcSpan.data(), srcSpan.data() + srcSpan.size(), destSpan.data());

    // Displaying copied data
    for (int value : destSpan) {
        std::cout << value << " ";
    }
    std::cout << "\n";

    return 0;
}

Explanation: In this example, std::copy works with raw pointers from srcSpan.data() and destSpan.data() to copy elements from src to dest. Although std::span abstracts away the need for direct pointers in many cases, data() allows you to use spans in pointer-based contexts, enabling compatibility with pointer-focused functions while retaining the benefits of std::span.

Summary of data()

  • Direct Access: data() provides a direct pointer to the underlying data, making std::span compatible with functions that require raw pointers.
  • Flexible Interfacing: Useful for low-level operations and working with legacy or third-party APIs.
  • Safety Retention: Allows pointer-based operations while maintaining bounds-checked safety in other parts of the code.

Using data() with std::span balances the power of low-level data manipulation with the safety of modern C++ practices, making it an essential feature for both high- and low-level programming tasks.

Leveraging std::span for Safe, Flexible Data Access

With its powerful capabilities for element access, iteration, and direct data handling, std::span is a valuable tool for developers seeking both flexibility and safety in data manipulation. Through the intuitive access methods (operator[], at(), front(), and back()), std::span makes working with elements straightforward and allows for bounds-checked access when needed. Iterator support, including begin(), end(), and their reverse versions, enables seamless integration with C++’s standard range-based loops and algorithms, making std::span adaptable to both high-level and low-level code requirements.

The data() function bridges the gap between high-level C++ constructs and low-level pointer-based APIs, allowing std::span to interface smoothly with legacy or external codebases that require raw pointers. This combination of safety, flexibility, and efficiency makes std::span a powerful tool for modern C++ programming.

Incorporating std::span into your codebase can help reduce errors, improve readability, and simplify function interfaces. As C++ evolves, features like std::span demonstrate the language’s ongoing commitment to balancing performance with safety, making it easier to write robust, efficient code that’s also easy to understand and maintain.


Check out all of the articles in this 5-part series:


Learn More about the C++ Standard Library! Boost your C++ knowledge with my new book: Data Structures and Algorithms with the C++ STL!


Discover more from John Farrier

Subscribe to get the latest posts sent to your email.

Discover more from John Farrier

Subscribe now to keep reading and get access to the full archive.

Continue reading