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::span
is 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 false
Code 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::span
that covers the firstcount
elements of the originalstd::span
.last(count)
: Creates a newstd::span
that covers the lastcount
elements of the originalstd::span
.subspan(offset, count)
: Creates a newstd::span
that starts from theoffset
and spanscount
elements 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::span
does 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 array
Code 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.