MemSpan

Synopsis

#include "swoc/MemSpan.h"

template<typename T>
class MemSpan

Reference documentation.

MemSpan is a view on writable memory. This distinguishes it from classes like TextView. The purpose of the class is to provide a compact description of a contiguous writeable block of memory, carrying both location and size information to (hopefully) prevent these from becoming separated. A MemSpan is a view because it never owns memory. It should be treated as a smart pointer that carries size information. As text views are intended to replace passing a character pointer and size separately, a memory span is intended to replace passing a pointer and a size as separate arguments.

MemSpan has two styles. The “void” style treats the memory as purely a block of memory without internal structure. The “typed” style treats the memory as an array of a specific type. These can be inter-converted, in the equivalent of type casting. The rules about conversions between span types is modeled on pointer conversions. Therefore a typed memory span implicitly converts to a void span, but a void span requires an explicit cast or rebinding to convert to a typed memory span.

As with pointers, a constant span can refer to writable memory and a non-constant span can refer to writable memory. Whether the memory referenced by the span is writable depends on whether the type of the span is a constant type. E.g. MemSpan<char> refers to writable memory and MemSpan<char const> does not.

Usage

A primary use of MemSpan is to generalize the type of a block of memory. For instance a function parameter could be std::vector<alpha> for some type alpha. Calling the function, however, requires the creation of a vector. This can require an unneeded memory allocation.

The other primary use is to work with chunks of memory embedded in larger containers. For instance in the previous example, not only is creating another vector needed, but it also makes working with subspans very difficult. Either copying is required or it can be worked around with various additional parameters, or by reverting to the most primitve and passing in a raw pointer and a count. It is much easier to have a MemSpan parameter type which handles all of these cases with minimal complexity. E.g. a function like

int f(MemSpan<char> source) { ... }

can be passed data from a vector, an array, a raw pointer and size, stack memory, etc., including types not defined in the scope of the function.

Void Span

A MemSpan<void> describes an undifferentiated contiguous block of memory. It can be constructed from either a pointer and length MemSpan< void >::MemSpan(value_arg *, size_t) or a half open range of two pointers MemSpan< void >::MemSpan(value_arg *, value_arg * ). A default constructed instance, or one constructed from nullptr has no memory and zero size.

The implementation differentiates between void and void const in order to be const correct. A void span implicitly converts to a void const span but not the other way as is the case for void* and void const* pointers. A void const span is the “universal” span - all other span types implicitly convert to this type.

The memory described by the instance can be obtained with the data method MemSpan< void >::data and the size with the size method MemSpan< void >::size().

Typed Span

A typed MemSpan treats the described memory as an array of the type. E.g., MemSpan<int> treats its memory as an array of int, with a subscript operator. The construction is the same as with the void case but the pointers must be of the span type. As with pointers and arrays, the count is the number of instances of T not the size in bytes.

Rebinding

A new MemSpan of a different type can be created from an existing instance. This is is called “rebinding” and uses the templated rebind method. This is roughly equivalent to type casting in that the original instance is unchanged. A new instance is created that refers to the same memory but with different type information. As an example, MemArena::alloc() returns a MemSpan<void>. If this is to be used for string data, then the call could look like

auto span = arena.alloc(n).rebind<char>();

This would make span of type MemSpan<char> covering the same memory returned from alloc.

Rebinding requires the size of the memory block to be an integral multiple of the size of the target type. That is, a binding that creates a partial object at the end of the array is forbidden.

Alignment

Rebinding on void spans can be done along with alignment using the align method. This can be done with a templated method for a type, or with an explicit alignment argument of type size_t. It does not fail due to partial objects. Instead enough space is discarded from the start of memory such that the returned span has the alignment required for T and memory space that would yield a partial object is discarded from the end. The result is the largest subspan that is memory aligned and has space for an integral number of instances.

Conversions

Memory spans will implicitly convert in a manner equivalent to raw pointers.

  • Non-const spans implicitly convert to constant spans of the same type.

  • Non-const spans implicitly convert to MemSpan<void>.

  • Any span will implicitly convert to MemSpan<void const>.

Construction

The most common style of construction is to provide a pointer and size / count. For void spans this is the number of bytes, while typed spans take a count as with arrays. E.g. for some type Alpha

Alpha array[6];

the corresponding span would be

MemSpan<Alpha> span(array, 6);

On the other hand the constructors understand arrays so this works as well

MemSpan<Alpha> span(array);

There is constructor support for any contiguous memory container that supports the data and size methods. A span can be contructed from std::string, std::string_view, or std::vector by passing the container. Internally this

std::vector<Alpha> av;
MemSpan<Alpha> aspan{av};

is treated as

std::vector<Alpha> av; MemSpan<Alpha> aspan{av.data(), av.size};

There are template deduction guides for std::array, std::vector, std::string, and std::string_view so that the previous could also be done as

std::vector<Alpha> av;
MemSpan aspan{av};