How it works...

In this recipe, we will explore what std::span is and why it is needed. In C++ (and even in C), to pass an array to a function, the following is implemented:

void foo(const int *array, size_t size)
{
for (auto i = 0; i < size; i++) {
std::cout << array[i] << ' ';
}

std::cout << ' ';
}

As shown in the preceding example, we have created a function called foo() that takes a pointer to an array as well as the size of the array. We then use this information to output the contents of the array to stdout.

We can execute this function as follows:

int main(void)
{
int array[] = {4, 8, 15, 16, 23, 42};
foo(array, sizeof(array)/sizeof(array[0]));
}

This results in the following output:

The problem with the preceding code is that it is not C++ Core Guideline-compliant. Specifically, we are forced to store the size of the array independently of the array itself. This can lead to issues if the array and its size become out-of-sync (something that is possible in large projects). The use of a pointer in relation to an array also prevents the use of ranged for loops, meaning we must manually traverse the array, which can also lead to potential stability issues if the for loop is not properly constructed. Lastly, we were required to calculate by hand the size of the array, an operation that, as shown, is prone to error, using sizeof().

One way to solve this issue is to use a template function, as follows:

template<size_t N>
void foo(const int (&array)[N])
{
for (auto i = 0; i < N; i++) {
std::cout << array[i] << ' ';
}

std::cout << ' ';
}

As shown in the preceding code snippet, we have defined a template function that takes a reference to an integer array of size N. We can then use N to traverse through this array. We can even use ranged for loops on the array since the compiler knows what the size of the array is at compile time. This code can be used as follows:

int main(void)
{
int array[] = {4, 8, 15, 16, 23, 42};
foo(array);
}

As shown here, we have made several improvements. We are no longer passing around pointers that could lead to NULL pointer violations. We are no longer calculating the size of the array by hand using sizeof(), and we no longer need to store the size of the array independently of the array itself. The problem with the preceding code is that each time the size of the array changes, we must compile a completely different version of the foo() function. If the foo() function is large, this could be a problem. This code also doesn't support dynamically allocated arrays (in other words, whether the array was allocated using std::unique_ptr).

To solve this, C++20 has added the std::span class. Check out this example:

void foo(const std::span<int> &s)
{
for (auto i = 0; i < s.size(); i++) {
std::cout << s[i] << ' ';
}

std::cout << ' ';
}

As shown in the preceding code snippet, we have created the foo() function using std::span, which stores an array of integers. Like most other C++ containers, we can get the size of the array, and we can use the subscript operator to access individual elements of the array. To use this function, we simply call it the same way we did using the template function, as follows:

int main(void)
{
int array[] = {4, 8, 15, 16, 23, 42};
foo(array);
}

Using std::span, we can now provide the same foo() function with arrays of different sizes, and we can even allocate the arrays using dynamic memory (in other words, std::unique_ptr) without having to re-implement the foo() function. Ranged for loops even work as expected:

void foo(const std::span<int> &s)
{
for (const auto &elem : s) {
std::cout << elem << ' ';
}

std::cout << ' ';
}

To use foo() with dynamic memory, we can do the following:

int main(void)
{
auto ptr1 = new int[6]();
foo({ptr1, 6});
delete [] ptr1;

std::vector<int> v(6);
foo({v.data(), v.size()});

auto ptr2 = std::make_unique<int>(6);
foo({ptr2.get(), 6});
}

As shown in the preceding example, we ran the foo() function with three different types of memory created dynamically. The first time we ran foo(), we allocated memory using new()/delete(). If you are attempting to remain C++ Core Guideline-compliant, you are likely not interested in this approach. The second and third approaches allocated the memory using std::vector or std::unique_ptr. Both have their inherent disadvantages:

  • std::vector stores its own size(), but also stores its capacity and, by default, initializes the memory.
  • std::unique_ptr doesn't store its own size(), and it, too, defaults initialized memory.

Currently, C++ does not have an array type capable of allocating a dynamic array of uninitialized memory while also storing the array's size (and only its size). std::span, however, can be used with some combination of the preceding approaches to manage an array depending on your needs.

It should also be noted that when we created std::span in the preceding example, we passed it the size of the array based on the total number of elements, not the total number of bytes. std::span is capable of providing both for you, as follows:

void foo(const std::span<int> &s)
{
std::cout << "size: " << s.size() << ' ';
std::cout << "size (in bytes): " << s.size_bytes() << ' ';
}

If we run the preceding implementation of foo(), with the aforementioned dynamic memory examples, we get the following:

Finally, we can use the span to create additional sub-spans, as follows:

void foo2(const std::span<int> &s)
{
for (const auto &elem : s) {
std::cout << elem << ' ';
}

std::cout << ' ';
}

In the preceding foo2() function, we take a span and output all of its elements using a ranged for loop. We can then use the following to create sub-spans:

void foo1(const std::span<int> &s)
{
foo2(s.subspan(5, 1));
}

The result of the subspan() function is another std::span. The difference is the fact that the pointer it stores internally has been advanced by 5 elements, and size() that the span stores is now 1.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset