Note
|
Precondition
namespace circular = trial::circular; |
The circular::span<T>
template class is a span that turns continguous storage
into a double-ended circular queue.
Span |
The storage is not owned by the span. The owner of the storage is responsible for destroying the span before releasing the storage. |
Circular |
Inserting elements into the span will remove elements at the other end of the span when the span is full. If elements always are inserted at the same end, then we effectively have a moving window over the most recent elements. |
Double-ended |
Elements can be inserted either at the beginning or the end of the span. This enables fixed-sized first-in-first-out or last-in-first-out usages. |
Queue |
Elements can only be inserted and erased at the beginning or end of the span. |
This tutorial demonstrates a reasonably simple use of a circular span where values are continuously inserted, but only the most recent ones are remembered and used.
Suppose we receive a sequence of numerical values \(x[n]\) one by one
over time \(n\), and we want to calculate the running average
\(\bar{x}[n]\) of the N
most recent values. This is easily done by
maintaining a running sum \(M_1[n]\).
Our class should at least support the following interface for inserting new values and for returning the current mean.
template <typename T, std::size_t N>
class average
{
public:
using value_type = T;
average() noexcept;
// Insert new value
void push(value_type value) noexcept;
// Return current mean
value_type mean() const noexcept;
};
When we insert a new value, the oldest value must be removed from our running
sum, so we have to remember the N
most recent values.
We therefore want to reserve storage for N
values, and have an efficient way
of inserting new values and removing old values from this storage.
We are going to use a circular span (circular::span<value_type, N> window
)
that uses an array (value_type storage[N]
) as storage.
template <typename T, std::size_t N>
class average
{
static_assert(N > 0, "N must be greater than zero");
public:
using value_type = T;
average() noexcept;
void push(value_type input) noexcept;
value_type mean() const noexcept;
private:
value_type sum = {};
value_type storage[N];
circular::span<value_type, N> window;
};
We start by initializing the circular span to use the array as storage.
template <typename T, std::size_t N>
average::average()
: window(storage) // Instruct span to use storage
{
}
Notice that the array is automatically filled with default-constructed elements. These will be overwritten one by one when inserting new values into the window.
When new values are inserted we first update the running sum. This is done by
undoing the effect of the oldest value (window.front()
) if the window is
full. There is a pre-condition for span<T, N>::front()
that it can only be
called if the window is not empty; this is guaranteed by the call to window.full()
.
Afterwards we store the input value in the window (window.push_back(input)
)
such that we can undo its effect when it later leaves the window. As the
push_back()
function overwrites the oldest value at the front, it is important
that we update the running sum before we store the input value.
template <typename T, std::size_t N>
void average::push(value_type input) noexcept
{
// Update the current mean
if (window.full())
{
sum += input - window.front();
}
else
{
sum += input;
}
// Remember the input value for later use
window.push_back(input);
}
The current mean is the calculated as the running sum divided by the number of elements in the window.
template <typename T, std::size_t N>
T average::mean() const noexcept
{
assert(!window.empty());
return sum / window.size();
}
This tutorial demonstrates how to use algorithms on the circular span. All we need are well-behaved iterators. The circular span iterators allows you to traverse all elements from the front to the back, or vice versa with reverse iterators. The iterators automatically handle that the elements may wrap around the underlying storage.
The Finite Impulse Response filter is one of the basic digital filters in the signal processing repertoire. A filtered value \(y[n]\) at time \(n\) is calculated as a weighted sum of input values \(x[i]\).
In other words, the filtered value is the inner product of the weights and the input values.
We need two containers - one for the weights and one for the input values.
The weights are given upon initialization, and the input values are accrued
over time. We are only interested in the last N
input values, so we use
a circular span to store the input values.
template <typename T, std::size_t N>
class impulse
{
static_assert(std::is_arithmetic<T>::value, "T must be arithmetic");
public:
using value_type = T;
template <typename... Weights>
impulse(Weights&&...);
// Insert new input value
void push(value_type);
// Return filtered value
value_type value() const;
private:
// Storage for weights
std::array<value_type, N> weights;
// Storage for input values
value_type storage[N];
circular::span<value_type, N> window;
};
The weights a passed to the constructor, which also pairs the span to its storage.
template <typename T, std::size_t N>
template <typename... Weights>
impulse::impulse(Weights... weights)
: weights(std::forward<Weights>(weights)...),
window(storage) // Instruct span to use storage
{
}
Accruing input values is done by pushing them into the span. The order of the weights are customarily specified starting with the weight \(w_0\) that is multiplied to the most recent input value \(x[n]\), and so on. We therefore insert the input values at the front of the span, so that when we iterate from the beginning towards the end, we will visit the most recent input values first.
template <typename T, std::size_t N>
void impulse::push(value_type input)
{
window.push_front(std::move(input));
}
Finally, calculating the filtered value is done using the inner product, which is were we need the iterators.
template <typename T, std::size_t N>
auto impulse::value() const -> value_type
{
// Function is const, so const_iterators are used.
return std::inner_product(window.begin(),
window.end(),
weights.begin(),
value_type{});
}
Other variations are possible. For instance, we could have pushed the input values at the end of the span, and then used reverse iterators in the algorithm.
This section describes the choices behind the design of the circular span.
The extent is an optional template argument that specifies the capacity of the span at compile-time. The capacity is part of the span type and therefore does not have to be stored as a member variable.
If the Extent
template argument is omitted, or specified as dynamic_extent
,
the capacity is determined when the span is constructed. The capacity does not
change after construction, unless the span is recreated by assignment. For this
case, the capacity is stored as a member variable.
The extent has been introduced for alignment with std::span<T, Extent>
.
The circular queue uses a lazy destruction policy when elements are removed from the queue. The removed elements are not destroyed immediately but linger in the underlying storage until they are overwritten by an insertion, or the underlying storage is destroyed.
-
When elements are removed by
remove_front()
orremove_back()
they are left untouched in the underlying storage. Only the internal state of the span is modified. -
When elements are popped by
pop_front()
orpop_back()
they are left in a moved-from state in the underlying storage.
In both cases the removed elements in the underlying storage are left in an unspecified but valid state, which enables us to overwrite them later on.
The reason is that the storage is contiguous, so there have to be some unused elements in the storage. The removed elements are therefore left in their position in the storage, and only destroyed when when the position is needed by another insertion. The removed elements are not replaced by some default element for performance reasons.
Circular span supports subspans, but unlike std::span
a subspans of a circular
span is not another circular span. Creating a circular span must be done on
contiguous storage. Although circular span operates on contiguous storage, the
range from begin()
until end()
is not guaranteed to be contiguous as it may
wrap around the underlying storage. Thus, a circular span cannot be constructed
from another circular span.
Instead span<T>::segment
and span<T>::const_segment
are used to represent
subspans. These types models the ContiguousRange and SizedRange requirements.
This means that they have both begin()
and end()
returning ContiguousIterator
and data()
and size()
to access the elements in the subspan.
The storage is not owned by the segment.
There are four member functions that returns a segment:
-
first_segment()
returns a range of all contiguous elements starting from the front of the span, -
last_segment
returns a range of any left-over elements that have been wrapped around in the underlying storage, -
first_unused_segment()
returns a range of all unused contiguous elements starting from the end of the span, and -
last_unused_segment()
returns a range of any left-over elements that have been wrapped around in the underlying storage.
Notice that first_segment()
will be non-empty if the span is non-empty, and
first_unused_segment()
will be non-empty is the span is non-full. The remaining
segments will be empty if their associated segment can contain all elements.
This functionality is useful for use cases such as zero-copy network transmission of the circular span.
Defined in header <trial/circular/span.hpp>
.
Defined in namespace trial::circular
.
template <
typename T,
std::size_t Extent = dynamic_extent
> class span;
The circular span template class is a circular view of some contiguous storage. The storage is not owned by the span. The owner must ensure that the span is destroyed before the storage is released.
The size is the current number of elements in the span.
The capacity is the maximum number of elements that can be inserted without overwriting old elements. The capacity cannot be changed.
The extent determines the capacity of the span.
With dynamic_extent
the capacity is derived from the input arguments
at construction or assignment time. Otherwise the capacity is fixed to the
specified Extent
template argument. Dynamic extent is used by default.
|
Element type.
|
|
The maximum number of elements in the span. |
Member type | Definition |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
RandomAccessIterator with |
|
RandomAccessIterator with |
|
|
|
|
|
ContiugousRange and SizedRange with |
|
ContiguousRange and SizedRange with |
Member function | Description |
---|---|
|
Creates an empty span with zero capacity.
|
|
Creates a span by copying.
|
|
Creates span by moving.
|
|
Creates a span by copying from convertible value type or compatible extent.
|
|
Creates a span from iterators.
|
|
Creates a span from iterators and initializes the span with the pre-existing |
|
Creates empty span from an array object with compatible extent.
|
|
Recreates span by copying.
|
|
Recreates span by moving.
|
|
Replaces span with elements from initializer list.
|
|
Checks if span is empty. |
|
Checks if span is full. |
|
Returns the maximum possible number of elements in the span. |
|
Returns the number of elements in the span. |
|
Returns a reference to the first element in the span.
|
|
Returns a constant reference to the first element in the span.
|
|
Returns a reference to the last element in the span.
|
|
Returns a constant reference to the last element in the span.
|
|
Returns a reference to the element at the given position in the span.
|
|
Returns a reference to the element at the given position in the span.
|
|
Clears the span.
|
|
Replaces the span with elements from iterator range.
|
|
Replaces the span with elements from initializer list.
|
|
Inserts an element at the beginning of the span.
|
|
Inserts elements from iterator range at the beginning of the span.
|
|
Inserts an element at the end of the span.
|
|
Inserts elements from iterator range at the end of the span.
|
|
Removes and returns an element from the beginning of the span.
|
|
Removes and returns an element from the end of the span.
|
|
Inserts the given number of unspecified elements at the beginning of the span.
|
|
Inserts the given number of unspecified elements at the end of the span.
|
|
Removes the given number of elements from the beginning of the span.
|
|
Removes the given number of elements from the end of the span.
|
|
Moves elements such that the span starts at the beginning of the storage.
|
|
Returns an interator to the beginning of the span. |
|
Returns an interator to the end of the span. |
|
Returns a reverse interator to the beginning of the span. |
|
Returns a reverse interator to the end of the span. |
|
Returns the first contiguous segment of the span.
|
|
Returns the last contiguous segment of the span.
|
|
Returns the first contiguous unused segment of the span.
|
|
Returns the last contiguous unused segment of the span.
|