Array
Array<T> turns a block of storage into a list you can append to. It does not allocate, it does not own its memory, and it never grows. You give it a buffer and a capacity; it tracks how much of that buffer is in use.
A Span<T> says: here are N elements. An Array<T> says: here is room for N elements, and M of them are live.
The problem it solves
Section titled “The problem it solves”Storage and occupancy are two different facts, and most code needs both.
When you allocate room for 512 draw commands, you do not have 512 draw commands. You have room for 512 and, at this moment, 37 real ones. A Span<T> cannot express that, because its size is its only number. Code built on a raw span therefore carries a separate count variable beside it, and must keep the two in step by hand.
Array<T> holds both numbers in one value and maintains the invariant count <= capacity.
// Two facts, two variables, kept in step by hand.Span<DrawCommand> storage = arena.Push<DrawCommand>(512);usize count = 0;
if (count < storage.size){ storage[count] = command; count += 1;}// One value that knows both.Array<DrawCommand> commands = Array<DrawCommand>::FromSpan(arena.Push<DrawCommand>(512));
commands.Append(command);It borrows, it does not own
Section titled “It borrows, it does not own”Array<T> never allocates and never frees. The allocation happens at the call site, where it is visible:
Array<Token> tokens = Array<Token>::FromSpan(arena.Push<Token>(1024));The array’s lifetime is bounded by its storage. If the storage came from an arena, rewinding that arena invalidates the array. If it came from a stack buffer, the array dies with the frame.
Because the array holds no allocator, it works over any storage: an arena push, a stack array, a fixed field inside a struct, a sub-range of a larger buffer. A reader of the call site also sees where the memory came from, without opening the array’s implementation.
It never grows
Section titled “It never grows”An append to a full array is a programming error, not an event the array handles by allocating. There is no growth factor, no reallocation, and no hidden allocator call. Two consequences follow:
- Pointers and spans into an array stay valid. A container that grows by reallocating invalidates every pointer into it.
Array<T>never reallocates, so you may hold aT*into it, or hand out aToSpan()and keep appending. - No append can allocate. So no append can fail for lack of memory, and none takes an unpredictable amount of time.
Appendcosts the same every time.
The cost: you must know a bound on the element count before you allocate. When you genuinely do not, Array<T> is the wrong type — see Choosing a sequence type.
Two ways to append
Section titled “Two ways to append”Each mutating operation comes in a strict form and a checked form. Which you use depends on whether a full array is a bug in your code or an expected condition.
| Form | On success | When full | Use it when |
|---|---|---|---|
Append(value) |
Adds the element | Asserts — this is a bug | You sized the storage, and overflow means you got it wrong |
TryAppend(value) |
Returns a pointer to the slot | Returns nullptr |
Input decides the count, and a full buffer is a normal outcome |
The batch forms follow the same split. AppendSpan asserts; TryAppendSpan returns false and adds nothing.
Removing elements
Section titled “Removing elements”Pop removes the last element and returns it. Order is preserved, since nothing else moves.
RemoveSwap removes an element at any index by moving the last element into its place. It runs in constant time and does not preserve order.
// Remove every dead entity in a single pass.for (usize i = 0; i < entities.size(); ){ if (entities[i].is_dead) { entities.RemoveSwap(i); // do not advance: a new element now sits at i } else { i += 1; }}There is no order-preserving remove. It would shift every following element, and for a bag of things where position carries no meaning, that cost buys nothing. When order does matter, sort at the point where order is needed.
Choosing a sequence type
Section titled “Choosing a sequence type”| You have | You want | Use |
|---|---|---|
| A known count, filled once | A range to read or write | Span<T> |
| A known bound, filled incrementally | Append, then read | Array<T> |
| No bound, built once then read | Append without a ceiling | ChunkList<T> |
| No bound, needs one contiguous block | Append, index, grow to gigabytes | VirtualArray<T> |
Choosing a container works through the question behind that table — who owns the storage — and covers the maps as well.
Element requirements
Section titled “Element requirements”Array<T> accepts trivially-copyable element types only, checked at compile time. It moves elements with a block copy and destroys nothing, which is what makes RemoveSwap and Clear constant-time operations.
Array<const T> does not compile: a read-only view is Span<const T>.
API summary
Section titled “API summary”Parameters, assertions and invariants are documented in basalt/core/Array.h.
Construction
Section titled “Construction”| Signature | Purpose |
|---|---|
Array<T>::FromSpan(storage) |
Wraps a span as an empty array whose capacity is the span’s size. |
Adding
Section titled “Adding”| Signature | Purpose |
|---|---|
Append(value) |
Adds one element. Asserts when full. |
TryAppend(value) |
Adds one element. Returns the slot, or nullptr when full. |
AppendSpan(values) |
Adds a block of elements. Asserts when they do not fit. |
TryAppendSpan(values) |
Adds a block of elements, or nothing. Returns whether it fit. |
Reading
Section titled “Reading”| Signature | Purpose |
|---|---|
operator[](index) |
Element access. Asserts on an out-of-range index. |
ToSpan() |
A span over the live elements only. |
size() |
The live element count. |
IsEmpty() / IsFull() |
Occupancy tests. |
GetRemainingCapacity() |
Free slots left. |
begin() / end() |
Range-for support over the live elements. |
Removing
Section titled “Removing”| Signature | Purpose |
|---|---|
Pop() |
Removes and returns the last element. Asserts when empty. |
RemoveSwap(index) |
Removes an element in constant time. Does not preserve order. |
Clear() |
Sets the live count to zero. Keeps the storage. |
Fields
Section titled “Fields”| Field | Purpose |
|---|---|
data |
The borrowed storage. Never owned, never freed by the array. |
count |
Live elements. Always <= capacity. |
capacity |
Fixed when the array is built. Never grows. |