Arene Base
Fundamental Utilities For Safety Critical C++
Loading...
Searching...
No Matches
Use arene::base::span instead of a pointer and length

It is a common pattern in code avoiding dynamic memory allocation is to have the allocation of a region of storage separate from the code that reads or writes to it. The address of this storage buffer, and the number of elements to read or write, is then passed into or returned from APIs that access it.

If the pointer to the buffer and length are passed around separately, then this is error-prone:

  • Length arguments can be confused with other integral arguments, especially if more than one buffer is being passed,
  • Buffer over-run checks have to be manually written for each access, which are easily forgotten,
  • Iterating over the elements in the buffer requires pointer arithmetic, which is against our coding standards,
  • It is easy for the supplied size to get out-of-sync with the real size of the storage, either due to a typo, or due to an incomplete change,
  • In particular, the pointer could be nullptr, even if the size is non-zero, leading to Undefined Behaviour from dereferencing a null pointer.

arene::base::span provides a safer alternative: it combines the pointer and length into a single object, so they are tied together. Access to the elements via the arene::base::span then does automatic range-checking for the indexing operator, preventing buffer over-runs. In addition, arene::base::span is a C++ range, so can be used with range-based for loops to iterate over the elements, or begin and end iterators can be obtained, to use with standard library algorithms. Finally, arene::base::span::subspan provides a bounds-checked way to obtain an arene::base::span that refers to a portion of the original range of elements.

Guideline: Replace uses of pointer+length with arene::base::span

Examples

Reading From a Buffer

You can use arene::base::span with a const element type as a replacement for read-only input buffers.

Before:

// Print the specified number of items from the supplied buffer
void print_items(int const* items, std::size_t count) {
for (std::size_t index = 0; index < count; ++index) {
std::cout << items[index] << "\n";
}
}
void some_caller() {
constexpr std::size_t count = 3;
int buffer[count] = {1, 2, 3};
print_items(buffer, count);
}

This loops over the items in the buffer in turn, but there is no bounds-checking. If the loop condition was mis-typed as index <= count, then the code will read off the end of the buffer.

After:

// Print all the items from the supplied buffer
void print_items(arene::base::span<int const> items) {
for (std::size_t index = 0; index < items.size(); ++index) {
std::cout << items[index] << "\n";
}
}
void some_caller() {
constexpr std::size_t count = 3;
int buffer[count] = {1, 2, 3};
print_items(buffer);
}

With arene::base::span, the size is directly associated with the pointer, so the accesses are bounds-checked. Now, if the loop condition was mis-typed as index <= items.size(), then the buffer over-run will be detected, and the program will abort with a precondition violation.

Note also that some_caller no longer needs to specify the count, because the entire array is being processed, and the arene::base::span can deduce the size of the span from the array bounds.

Given that print_items doesn't actually use the index other than for indexing into the span, the code can be improved further, and use the range-based for loop instead, thus eliminating the indexing entirely:

// Print all the items from the supplied buffer
void print_items(arene::base::span<int const> items) {
for (auto& element : items) {
std::cout << element << "\n";
}
}

Returning A Portion of a Buffer

Functions that fill a buffer, like read and snprintf, need some mechanism for communicating how much of the buffer was filled. Such functions can end up using a combination of the return value and "out" parameters to communicate the desired data. This can lead to complications in the API, and a separation of the size of the valid data from the pointer to the valid data.

arene::base::span can also be used to simplify this use case. For example, the arene::base::filesystem::file_handle::sequential_read function reads data from a file into a supplied buffer, and returns an arene::base::result holding an arene::base::span on success. The returned arene::base::span covers the region of the supplied buffer that was populated by the call.

void process_read_data(arene::base::span<arene::base::byte> data);
void read_and_process_data(arene::base::filesystem::file_handle file) {
constexpr std::size_t buffer_size = 100;
std::array<arene::base::byte, buffer_size> buffer;
auto read_result = file.sequential_read(buffer);
if (!read_result) {
handle_error(read_result.error());
} else {
auto read_data = *read_result;
std::cout << "Read " << read_data.size() << " bytes\n";
process_read_data(read_data);
}
}

Here, the only reference to buffer is in the call to sequential_read. The code that handles the result does not need to know where the buffer is, since it is encapsulated inside the returned arene::base::span. This reduces the potential for errors, and simplifies the code, compared to equivalent code using read.

The implementation of sequential_read also demonstrates how arene::base::span::subspan can be used to obtain a span referencing the populated data:

auto file_handle::sequential_read(span<byte> buffer) const noexcept -> result<span<byte>, error_code> {
// parasoft-begin-suppress AUTOSAR-M19_3_1-a "read uses errno to report errors"
// parasoft-begin-suppress AUTOSAR-A17_1_1-a "read uses errno to report errors"
errno = 0;
// parasoft-end-suppress AUTOSAR-M19_3_1-a
// parasoft-end-suppress AUTOSAR-A17_1_1-a
::ssize_t const read_size{::read(fd_, buffer.data(), buffer.size())};
if (read_size < 0) {
return error_result(error_code::from_errno());
}