Skip to content

← Unsafe and Soundness step 16 of 24

Hard Framework

Build a vector with allocation, push and pop

Implement an owning vector using the supplied pointer, length and capacity fields. Store elements in manually allocated memory and expose safe operations.

pub struct MyVec<T> { ptr: NonNull<T>, len: usize, cap: usize }
pub fn vec_script(ops: Vec<String>) -> Vec<String>

Complete grow, push, pop, get and Drop. Add Default, is_empty, and the bounded Send and Sync implementations described below. Support non-Copy values without cloning them. Keep the supplied script interpreter. new deliberately rejects zero-sized types; you do not need to support them.

Required behavior

A new vector has length and capacity zero. Its first push allocates four slots. Whenever a push finds len == cap, double the capacity before writing. Popping elements, including the last one, does not reduce capacity.

push appends a value. pop transfers ownership of the last value to the caller, or returns None when empty. get(index) returns a reference tied to the vector borrow, or None when index >= len. is_empty reports len == 0, and Default produces the same empty vector as new.

The script starts with an empty MyVec<String> and returns one string per operation:

Command Output
push S Append S and return "ok".
pop Return the last string, or "none" when empty.
len Return the number of elements as decimal text.
cap_at_least N Return "true" if capacity is at least N, otherwise "false".
get I Return the indexed string, or "none" when missing.
Unknown command Return "?".

The parser splits at the first literal ASCII space. A push stores the entire remainder without trimming, including an empty string when no remainder is present. pop and len ignore the remainder. Numeric fields use Rust's usize parser, including a leading +. Invalid or unrepresentable capacity thresholds default to zero; invalid indices give "none". Empty scripts return no results. A stored string "none" is indistinguishable from the missing-value marker in this output format.

commands: ["cap_at_least 1", "push a", "cap_at_least 4", "cap_at_least 5", "pop", "cap_at_least 4"]
results:  ["false",          "ok",     "true",           "false",          "a",   "true"]

Track initialized storage

Maintain len <= cap. Slots before len contain owned, initialized values. Slots from len to cap are spare storage and must not be read or dropped as values. With zero capacity, use NonNull::dangling() as a non-null, aligned sentinel; there is no allocation to free.

NonNull<T> is covariant in T. An owning collection can remain covariant even when it provides mutable access through &mut self: that exclusive borrow prevents conflicting use during mutation. Returning mutable access does not automatically require making the owning type invariant. See the Rustonomicon's variance explanation.

Allocate and grow

Construct layouts with Layout::array::<T>(capacity). Use alloc for the first nonempty buffer and realloc with the old layout and new byte size for later growth. On successful reallocation, use the returned pointer; the old pointer must no longer be used. Update capacity to match that allocation.

Layout construction rejects arithmetic overflow and layouts exceeding its isize-based size bound. The supplied explicit size assertion is an additional statement of the invariant, not a size check missing from Layout::array. Reject impossible growth before changing ownership state. See Layout::array.

Route a null allocation result to alloc::handle_alloc_error. It does not return normally. Avoid assuming that its termination behavior is always the same under every environment and configuration; see handle_alloc_error.

Move values into and out of slots

After ensuring capacity, push writes the new value with ptr::write, then increments len. Writing this way does not attempt to drop an old value in uninitialized storage.

For a nonempty pop, decrement len and read the former final slot with ptr::read. The returned value belongs to the caller; the vector must not drop that slot again. Raw reads and writes do not invoke Clone. The remaining bytes beyond len are no longer an owned element of the vector.

Destroy elements, then release the buffer

Drop the initialized values and deallocate with the layout for the current capacity. Never deallocate the dangling sentinel, and never run destructors over spare capacity. A pop loop is one simple cleanup strategy; it depends on correct bookkeeping just like any other implementation.

With nonpanicking destructors, each retained element must be dropped exactly once. The reference's simple loop can leak the remaining elements and buffer if an element destructor panics. This exercise does not reproduce all of standard Vec's panic-cleanup machinery.

Justify thread bounds

NonNull does not make its containing type Send or Sync automatically. For this owning API, implement Send when T: Send and Sync when T: Sync, with safety comments explaining ownership and shared access. Do not omit the bounds: a shared or moved collection must respect the restrictions of its elements.

The script checks values, lengths and capacity thresholds. It does not prove allocation correctness or soundness. Also test non-Clone elements with destructor counters, out-of-range access and reuse after popping to empty. Passing requires compilation, test cases and Clippy with warnings denied.