C++, a language known for its performance and efficiency. With each new standard release, it offers more features and tools that make coding in it more robust and convenient. One such tool, introduced in C++20, is std::span.
In this practical guide, we will delve into std::span and its utility in contemporary C++ programming. This lightweight, non-owning reference to an array or a part of an array has the potential to streamline your code, reducing redundancy and enhancing clarity.
Whether you’re working with arrays, vectors, or other data structures, std::span can be a versatile tool in your C++ toolbox. It bridges the gap between dynamic and static containers, providing a degree of flexibility that C++ programmers have long sought.
Through the course of this article, we will explore what std::span is, why it’s a valuable addition to C++20, and how to use it effectively in your code. This guide is packed with real-world examples to help you grasp the concept and start applying it in your own projects.
Basics of std::span
To appreciate the value std::span brings to C++20, we first need to understand its definition and purpose. At its core, std::span is a simple tool designed to enhance the way we handle data sequences in C++.
std::span is a lightweight, non-owning reference to a sequence – be it an array or a part of an array. The term ‘non-owning’ is crucial here. It implies that std::span does not manage the lifetime of the objects it refers to. Instead, it merely provides a view or a reference to an existing sequence of data. This feature makes std::span an efficient tool for passing around views of contiguous sequences (like arrays) without having to copy or clone them.
It’s also worth noting that std::span is not restricted to static arrays. It can work with dynamic data structures like std::vector and std::array, making it a highly versatile tool that works seamlessly with both dynamic and static containers.
In essence, std::span serves as a bridge between dynamic and static worlds, offering a unified, efficient way to handle data sequences in C++. It’s a testament to the C++ ethos of zero-overhead abstraction – you get a high-level view of your data without sacrificing performance.
Creating and Initializing std::span
Creating and initializing std::span is straightforward and intuitive. To define a std::span, you need to provide the type of elements the std::span will reference and optionally the extent of the std::span, which is the number of elements it can hold.
Here is a basic example of defining a std::span:
std::span<int> my_span;Code language: C++ (cpp)In this case, my_span is a std::span that refers to a sequence of int elements. Note that at this point, my_span doesn’t yet reference any actual data.
To initialize a std::span, you can use an array, a pointer with a size, two pointers, or a container like std::vector or std::array. Let’s take a look at some examples:
Initializing with an array:
int my_array[5] = {1, 2, 3, 4, 5};
std::span<int> span_from_array(my_array);Code language: C++ (cpp)Initializing with two pointers:
int my_array[5] = {1, 2, 3, 4, 5};
std::span<int> span_from_pointers(&my_array[0], &my_array[4]);Code language: C++ (cpp)Initializing with a pointer and size:
int my_array[5] = {1, 2, 3, 4, 5};
std::span<int> span_from_pointer_and_size(my_array, 5);Code language: C++ (cpp)Initializing with a container:
std::vector<int> my_vector = {1, 2, 3, 4, 5};
std::span<int> span_from_vector(my_vector);Code language: C++ (cpp)Remember, std::span does not own the data it refers to. So if the data it points to is destroyed or goes out of scope, the std::span will be left dangling. Always be mindful of the lifetime of your data when working with std::span.
Accessing Elements in std::span
Just like with other containers in C++, you can easily access the elements in a std::span. You can use the array subscript operator ([]) or the at() member function, similar to how you would with an array or std::vector.
Here’s how you might do it:
std::vector<int> my_vector = {1, 2, 3, 4, 5};
std::span<int> my_span(my_vector);
// Accessing elements
int first_element = my_span[0];
int second_element = my_span.at(1);Code language: C++ (cpp)Note that while the array subscript operator ([]) does not perform bounds checking, the at() function does and will throw an exception (std::out_of_range) if you attempt to access an element outside the valid range.
In addition to accessing individual elements, std::span provides several utility functions to work with the sequence:
size(): Returns the number of elements in thestd::span.length(): This is an alias forsize(). It also returns the number of elements in thestd::span.empty(): Checks if thestd::spanis empty (i.e., whether its size is 0).
Here’s an example:
std::vector<int> my_vector = {1, 2, 3, 4, 5};
std::span<int> my_span(my_vector);
// Using utility functions
size_t num_elements = my_span.size(); // returns 5
bool is_empty = my_span.empty(); // returns falseCode language: C++ (cpp)Manipulating std::span
std::span is a versatile tool, not just for accessing elements, but also for manipulating the viewed sequence. It offers several functions that allow us to create subviews of the original sequence, essentially providing us with slices of our data. These functions include first(), last(), and subspan().
first(count): Creates a newstd::spanthat covers the firstcountelements of the originalstd::span.last(count): Creates a newstd::spanthat covers the lastcountelements of the originalstd::span.subspan(offset, count): Creates a newstd::spanthat starts from theoffsetand spanscountelements of the originalstd::span.
Here are some examples of these functions in action:
std::vector<int> my_vector = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::span<int> my_span(my_vector);
// Using first() to create a subspan
std::span<int> first_half = my_span.first(5); // Contains {1, 2, 3, 4, 5}
// Using last() to create a subspan
std::span<int> last_half = my_span.last(5); // Contains {6, 7, 8, 9, 10}
// Using subspan() to create a subspan
std::span<int> middle = my_span.subspan(3, 4); // Contains {4, 5, 6, 7}Code language: C++ (cpp)Remember, these subspans are just views of the original data. They don’t own the data they’re viewing, and they don’t copy or move anything around. This makes these operations extremely efficient.
Using std::span with Other STL Containers
One of the great benefits of std::span is its ability to work seamlessly with other Standard Template Library (STL) containers. You can initialize a std::span with any STL container that provides contiguous storage, like std::vector or std::array.
Here are some examples:
// With std::vector
std::vector<int> my_vector = {1, 2, 3, 4, 5};
std::span<int> span_from_vector(my_vector);
// With std::array
std::array<int, 5> my_array = {1, 2, 3, 4, 5};
std::span<int> span_from_array(my_array);Code language: C++ (cpp)In these examples, span_from_vector and span_from_array provide a view of the data stored in my_vector and my_array, respectively. You can then use this std::span to access and manipulate the data in a unified manner, regardless of whether the underlying container is a std::vector or a std::array.
This unified access is one of the key strengths of std::span. It allows you to write generic code that operates on a sequence of data, without caring about the specific type of the underlying container. This can greatly enhance the reusability and flexibility of your code.
Using std::span in Functions
A common use case for std::span is as function parameters. When a function needs to operate on a sequence of data, it can take a std::span as a parameter. This allows the function to be agnostic about the type of container that stores the data, as long as it provides contiguous storage.
This is advantageous for several reasons:
- Flexibility: The function can accept any type of container (like
std::array,std::vector, or even a C-style array), enhancing its reusability. - Efficiency: As
std::spandoes not own the data it points to and does not involve any deep copying, it’s a lightweight and efficient way of passing around views of data. - Clarity: By using
std::span, you signal to the readers of your code that the function does not take ownership of the data and does not modify its lifetime.
Here’s an example of a function that takes a std::span as a parameter:
void print_elements(std::span<int> numbers) {
for (int number : numbers) {
std::cout << number << " ";
}
std::cout << std::endl;
}
// Usage with different containers:
std::vector<int> my_vector = {1, 2, 3, 4, 5};
print_elements(my_vector); // Can pass std::vector
std::array<int, 5> my_array = {6, 7, 8, 9, 10};
print_elements(my_array); // Can pass std::array
int my_c_array[5] = {11, 12, 13, 14, 15};
print_elements(my_c_array); // Can pass C-style arrayCode language: C++ (cpp)Safety Considerations and Limitations of std::span
While std::span is a powerful tool, it’s important to remember the safety considerations and limitations that come with it.
Safety Considerations
The main safety concern with std::span is its non-owning nature. std::span provides a view into a sequence of data but does not own or manage the lifetime of that data. This means that if the original data is modified or destroyed while a std::span is still pointing to it, the std::span will be left dangling. This can lead to undefined behavior and hard-to-track bugs.
Therefore, it’s crucial to always be mindful of the lifetime of your data when working with std::span. Avoid returning std::span from functions if there’s any chance that the data it points to could go out of scope.
Limitations
std::span can only handle sequences that provide contiguous storage. This means it can work with arrays, std::array, std::vector, and similar containers, but not with containers like std::list or std::map that do not guarantee contiguous storage.
Furthermore, std::span is not designed to replace other STL containers. It lacks features like memory management, dynamic resizing, or element insertion and deletion. Instead, std::span is meant to complement these containers, providing a lightweight and efficient way to view and pass around their data.
