Exploring C++ std::span – Part 3: Subviews and Slices

One of the most powerful features of std::span is its ability to create “subviews,” or smaller views over specific portions of a data range. This functionality allows you to slice data efficiently without creating new arrays or vectors, enabling you to work with subsections of data in a safe and controlled manner. Using std::span, you can extract and manipulate parts of arrays, vectors, or other contiguous memory without the overhead of copying, making it ideal for applications that require precise data segmentation.

In this article, we will continue our exploration of std::span. You can review the previous articles first if you need to catch up your knowledge:

In this post, we’ll explore how to create subviews using methods like first(), last(), and subspan(). These functions make it easy to split data into manageable sections, facilitating operations like parallel processing, data partitioning, and targeted manipulation of specific data regions. We’ll also look at practical examples where subviews can simplify working with large data sets or structured information, showing how std::span provides a lightweight, efficient alternative to traditional slicing techniques.

By the end of this article, you’ll understand how to harness std::span’s subview capabilities to streamline your code, enhance performance, and improve safety when working with partial views of data.

Creating Subviews of Spans

std::span includes several methods that allow you to create subviews, or “slices,” of data from an existing span. These methods—first(), last(), and subspan()—enable you to reference only a specific part of a span, making it easy to focus on a subset of elements without copying or modifying the original data.

Using first()

The first(count) method creates a subview consisting of the first count elements of a span. This is helpful when you want to work only with the leading portion of data.

#include <span>
#include <iostream>

int main() {
    int data[] = {1, 2, 3, 4, 5, 6, 7, 8};
    std::span<int> mySpan(data);  // Full span over the array

    // Creating a subview of the first 3 elements
    std::span<int> firstThree = mySpan.first(3);

    std::cout << "First three elements: ";
    for (int value : firstThree) {
        std::cout << value << " ";
    }
    std::cout << "\n";
    return 0;
}

Explanation: In this example, mySpan.first(3) creates a new span firstThree that references only the first three elements of data. This approach is lightweight and efficient because no new memory allocation or data copying is involved; the span simply points to the same underlying data, but only shows a portion of it.

Using last()

The last(count) method creates a subview of the last count elements in a span. This can be useful when you need to focus on data at the end of a sequence.

#include <span>
#include <iostream>

int main() {
    int data[] = {10, 20, 30, 40, 50, 60, 70, 80};
    std::span<int> mySpan(data);  // Full span over the array

    // Creating a subview of the last 4 elements
    std::span<int> lastFour = mySpan.last(4);

    std::cout << "Last four elements: ";
    for (int value : lastFour) {
        std::cout << value << " ";
    }
    std::cout << "\n";
    return 0;
}

Explanation: mySpan.last(4) creates a span lastFour that references only the last four elements of data. Like first(), last() provides a view over part of the data without copying, making it an efficient choice for working with trailing segments of a collection.

Using subspan()

The subspan(offset, count) method creates a subview starting at a specific offset and extending for count elements. If count is omitted, the subview extends from offset to the end of the span. This flexibility makes subspan() ideal for slicing arbitrary segments within a span.

#include <span>
#include <iostream>

int main() {
    int data[] = {5, 10, 15, 20, 25, 30, 35, 40, 45};
    std::span<int> mySpan(data);  // Full span over the array

    // Creating a subview of 4 elements, starting at index 2
    std::span<int> midFour = mySpan.subspan(2, 4);

    std::cout << "Four elements from index 2: ";
    for (int value : midFour) {
        std::cout << value << " ";
    }
    std::cout << "\n";
    return 0;
}

Explanation: In this example, mySpan.subspan(2, 4) creates a span midFour starting at index 2 of data and covering four elements. By setting an offset and specifying the count of elements to include, subspan() gives you fine-grained control over which part of the data you want to work with.

These methods make std::span ideal for applications that require efficient, targeted access to parts of a dataset. Using subviews, you can focus on specific sections of data without creating additional copies, optimizing your programs’ performance and memory usage.

Use Cases for Subviews

The subview capabilities of std::span are particularly useful in scenarios where you need to work with specific sections of data without duplicating or modifying the original dataset. Subviews enable you to create lightweight, efficient views into subsets of data, which can be beneficial in a variety of applications. Here are some common use cases where std::span‘s subviews are especially valuable:

Partitioning Data for Parallel Processing

When processing large datasets, it’s often beneficial to split data into chunks that can be handled in parallel. Using std::span, you can create multiple subviews to represent each chunk, allowing for efficient, parallel processing of different sections of the same dataset. This approach is memory-efficient because it avoids copying data, and it’s safe because each std::span subview ensures bounds-checking.

#include <span>
#include <iostream>
#include <thread>
#include <vector>

void processChunk(std::span<const int> chunk) {
    for (int value : chunk) {
        std::cout << value << " ";
    }
    std::cout << "\n";
}

int main() {
    int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::span<int> fullSpan(data);

    // Creating two subviews to represent two chunks
    std::span<int> firstHalf = fullSpan.first(fullSpan.size() / 2);
    std::span<int> secondHalf = fullSpan.subspan(fullSpan.size() / 2);

    // Processing chunks in parallel
    std::thread t1(processChunk, firstHalf);
    std::thread t2(processChunk, secondHalf);

    t1.join();
    t2.join();

    return 0;
}

Explanation: Here, we divide data into two chunks, firstHalf and secondHalf, using first() and subspan(). Each chunk is processed in parallel by separate threads. std::span allows for efficient partitioning and safe access to each segment without modifying or copying the underlying data​.

Safe Access to Parts of Arrays in Larger Functions

In complex functions that handle large arrays or datasets, it’s often necessary to work only with specific parts of the data. By using subviews, you can focus on relevant segments without complicating function logic or risking out-of-bounds access.

For example, if a function processes sensor data, you might want to split that data into recent readings and historical readings, each handled separately:

#include <span>
#include <iostream>

void analyzeRecentData(std::span<const int> recentData) {
    std::cout << "Analyzing recent data: ";
    for (int value : recentData) {
        std::cout << value << " ";
    }
    std::cout << "\n";
}

void analyzeHistoricalData(std::span<const int> historicalData) {
    std::cout << "Analyzing historical data: ";
    for (int value : historicalData) {
        std::cout << value << " ";
    }
    std::cout << "\n";
}

int main() {
    int data[] = {11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
    std::span<int> fullSpan(data);

    // Split data: last 3 elements are "recent" data, the rest is "historical"
    std::span<int> recentData = fullSpan.last(3);
    std::span<int> historicalData = fullSpan.first(fullSpan.size() - 3);

    analyzeRecentData(recentData);
    analyzeHistoricalData(historicalData);

    return 0;
}

Explanation: In this example, we create two subviews: recentData and historicalData. This approach makes the function logic cleaner and more focused, as each function only receives the data it needs to process, ensuring separation of concerns while avoiding unnecessary data duplication​.

Flexible Data Slicing in Algorithms

For algorithms that operate on sections of data, such as sliding windows or moving averages, std::span enables efficient slicing and reuse of overlapping data segments. This is particularly useful for processing streams or running statistical analyses on contiguous segments of data.

#include <span>
#include <iostream>

void calculateAverage(std::span<const int> segment) {
    int sum = 0;
    for (int value : segment) {
        sum += value;
    }
    std::cout << "Average of segment: " << (sum / segment.size()) << "\n";
}

int main() {
    int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::span<int> fullSpan(data);

    // Calculate average of overlapping segments
    for (std::size_t i = 0; i <= fullSpan.size() - 3; ++i) {
        calculateAverage(fullSpan.subspan(i, 3)); // Overlapping subspan
    }

    return 0;
}

Explanation: In this example, we calculate the average of overlapping segments in data using subspan(i, 3) in a loop. This approach is both memory- and performance-efficient, as each subspan() call reuses the underlying array without creating new allocations. This is ideal for algorithms that require sliding windows or moving data segments​.

Summary of Subview Use Cases

  • Parallel Processing: Partition large data sets into smaller chunks for concurrent processing.
  • Targeted Function Input: Restrict function inputs to only the relevant segments of data, simplifying function logic.
  • Sliding Windows and Overlapping Segments: Efficiently handle overlapping data slices in algorithms without copying data.

By using first(), last(), and subspan(), std::span makes data segmentation simple and efficient, helping you work with large datasets or complex data processing tasks without creating unnecessary copies.

Real-World Example: Managing Sections of a Dataset with std::span

To illustrate the power of std::span in real-world applications, let’s walk through an example where we manage and process specific sections of a dataset without duplicating data. In this scenario, we’ll assume that we’re working with a dataset containing time-series sensor readings, such as temperature values, which are stored in an array.

Using std::span, we’ll create subviews to analyze different parts of the dataset separately. For instance, we might want to compute the average temperature over recent readings and detect anomalies in historical data.

Scenario: Analyzing Recent and Historical Sensor Data

In this example, we have a continuous stream of temperature data stored in an array. The dataset is split into two parts:

  • Recent readings: The latest portion of the data to calculate real-time metrics, like the average temperature.
  • Historical readings: An older portion of the data to check for trends or anomalies.

Let’s use std::span to access and analyze each section.

#include <span>
#include <iostream>
#include <numeric>  // For std::accumulate

// Function to calculate the average of a span
double calculateAverage(std::span<const int> readings) {
    if (readings.empty()) return 0.0;
    int sum = std::accumulate(readings.begin(), readings.end(), 0);
    return static_cast<double>(sum) / readings.size();
}

// Function to check for anomalies in historical data
void detectAnomalies(std::span<const int> historicalData) {
    int threshold = 30;  // Example threshold for temperature anomaly
    bool foundAnomaly = false;

    for (int temp : historicalData) {
        if (temp > threshold) {
            std::cout << "Anomaly detected: " << temp << "\n";
            foundAnomaly = true;
        }
    }
    
    if (!foundAnomaly) {
        std::cout << "No anomalies detected in historical data.\n";
    }
}

int main() {
    int temperatures[] = {25, 26, 28, 27, 29, 31, 33, 32, 30, 35, 34, 36, 37, 33, 28};
    std::span<int> fullData(temperatures);

    // Define recent and historical subviews
    std::span<int> recentData = fullData.last(5);        // Last 5 readings
    std::span<int> historicalData = fullData.first(10);  // First 10 readings

    // Calculate and display average temperature of recent readings
    double averageRecent = calculateAverage(recentData);
    std::cout << "Average temperature of recent data: " << averageRecent << "\n";

    // Detect anomalies in historical readings
    detectAnomalies(historicalData);

    return 0;
}

View on Godbolt.org

Explanation of the Code

  1. Defining Recent and Historical Data Views: We use last(5) to get the most recent five readings and first(10) to get the first ten readings as historical data. These subviews allow us to split the dataset for separate analysis without copying or modifying the underlying data.
  2. Calculating the Average: In calculateAverage, we compute the average of the recent readings by summing the elements within the recentData span. This operation is efficient because the span provides direct access to the data without any duplication or additional memory allocation.
  3. Detecting Anomalies: In detectAnomalies, we check each temperature in the historicalData span to see if it exceeds a set threshold, indicating an anomaly. If a reading exceeds the threshold, we print an alert message; otherwise, we report that no anomalies were detected.

Benefits of Using std::span for This Scenario

  • Memory Efficiency: std::span allows us to create views into the data without copying. This makes the code memory-efficient and avoids unnecessary duplication, especially important when dealing with large datasets.
  • Code Clarity: By separating recent and historical data using subviews, the code remains clear and focused. Each function (calculateAverage and detectAnomalies) operates only on the relevant subset of data, making it easier to understand and maintain.
  • Safety: std::span provides bounds-checked access with functions like first() and last(), reducing the risk of accessing data out of bounds.

In this example, std::span allows us to efficiently and safely manage sections of a dataset. By creating specific subviews, we can analyze recent and historical data independently while maintaining a single, contiguous data source. This capability makes std::span a powerful tool for applications that require efficient data handling and modular analysis, such as time-series processing, financial analysis, or real-time monitoring systems.

Efficient Data Slicing and Segmentation with std::span

In this article, we explored how std::span enables efficient, safe access to specific sections of contiguous data without duplicating memory. By using methods like first(), last(), and subspan(), we can create targeted subviews over data, simplifying complex operations such as partitioning for parallel processing, working with sliding windows, and analyzing specific parts of large datasets.

We demonstrated practical uses for subviews with examples ranging from parallel processing and anomaly detection to real-time data analysis. This modular approach not only improves code clarity but also optimizes performance by avoiding unnecessary memory allocation. With std::span, we can quickly and safely slice data into manageable sections, allowing focused and efficient processing that supports the needs of high-performance applications.

In the next article, we’ll dive into const-correctness and type safety with std::span. We’ll discuss how std::span enables you to control read-only and mutable access to data through std::span<const T> and std::span<T>, promoting safer, clearer interfaces for your functions. By understanding the differences and best practices around these features, you’ll be able to leverage std::span more effectively in a variety of coding scenarios.


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.

Leave a Reply

Discover more from John Farrier

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

Continue reading