We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 24 of 24
Build a ring buffer with MaybeUninit
Complete a fixed-capacity, double-ended ring buffer using
Box<[MaybeUninit<T>]>. Keep the supplied ring_script wrapper, which exercises
the buffer with owned strings.
Implement push_front, pop_back, get and Drop, and fix the unsafe read
in pop_front. The allocation, capacity queries and push_back are supplied.
The buffer must support non-Copy, non-Clone values without cloning them.
| Method | Required behavior |
|---|---|
push_back(value) / push_front(value) |
Insert at that end and return true; return false when full |
pop_front() / pop_back() |
Remove and return that end's value, or None when empty |
get(i) |
Borrow the value at logical index i, or return None when i >= len |
Drop |
Drop values still owned by the buffer, without reading unused slots |
A rejected push consumes and drops its argument; this API returns a boolean,
not the rejected value. Capacity may be zero or one. At zero capacity, pushes
fail, pops and lookups return None, and no indexing or modulo is needed.
Track owned values, not just bytes
Maintain len <= capacity. When capacity is nonzero, head identifies the
first live slot. Logical element i, for i < len, is stored at
(head + i) % capacity.
The live window contains exactly the values owned by the buffer. Other slots
must not be read or dropped as owned Ts. A popped slot may still contain the
old bytes, but those bytes do not grant ownership of another value.
For capacity four, head = 3 and len = 2 means physical slots 3 and 0
are live, in that order. The next back insertion uses slot 1; a front
insertion uses slot 2. This is why traversing 0..len as physical indices
fails after wraparound.
Use MaybeUninit's operations deliberately
MaybeUninit<T> can hold storage without a valid T in it. Dropping the
wrapper does not drop a contained T, so your buffer must manage that work.
It has the same size and alignment as T; that does not make uninitialized
storage safe to treat as a T.
write(value)initializes a slot without dropping its previous contents. Use it for an available slot; overwriting a live value would leak that value.assume_init_read()reads an initialized value out. Treat this as an ownership transfer and remove that slot from the live window.assume_init_ref()borrows an initialized value. The returned reference must remain valid for its borrow of the ring.assume_init_drop()drops an initialized value in place. That value must not be dropped again.
The assume_init operations are unsafe because the caller must establish the
relevant initialization, ownership and borrowing conditions. Add a SAFETY
comment explaining those conditions at each unsafe block; the enabled Clippy
lint checks for documentation, not whether the argument is correct.
The constructor creates each uninitialized slot separately. Repeating a slot
with vec![MaybeUninit::uninit(); cap] would require a Clone implementation
for MaybeUninit<T>, which is available when T: Copy. It cannot be used for
this unconstrained generic buffer or for String.
Update the window around ownership changes
Guard full or empty cases before computing a slot. Avoid subtracting one
directly from a zero usize head. For the exercise's small capacities,
(head + capacity - 1) % capacity wraps backward; for unrestricted large
capacities, also consider overflow in additions before the modulo.
Write an inserted value before publishing it through head and len.
When popping, ensure the transferred value is no longer recorded as owned by
the ring before anything can panic or call user code. The supplied front pop
reads the value and then updates its indices without an intervening user call;
do not add a panicking operation between those steps.
These pushes accept an already-created T: they do not clone it or grow the
allocation. If a future API creates values through a closure, that introduces
an additional panic boundary to account for.
Drop the live window
Reuse the removal logic to drain values still in the ring:
impl<T> Drop for Ring<T> {
fn drop(&mut self) {
while self.pop_front().is_some() {}
}
}
With non-panicking element destructors, this releases each remaining value once, including when the ring is dropped during an unrelated unwind. Popped values belong to the caller and must not be dropped by the ring again.
This loop does not guarantee leak-free cleanup if a T::drop panics: the
loop stops, and the remaining MaybeUninit slots do not drop their contents.
A stronger cleanup guarantee requires additional unwind handling. A second
panic during an existing unwind can abort the process.
Script behavior and verification limits
ring_script(cap, ops) returns one string per command:
| Command | Output |
|---|---|
push_back S / push_front S |
"ok" or "full" |
pop_front / pop_back |
Removed string or "none" |
len |
Decimal length |
join |
Live strings in front-to-back order, separated by commas |
| Unknown command | "?" |
Commands split at the first ASCII space. Pushes preserve the entire remaining
text, including additional spaces; with no argument they push an empty string.
join on an empty buffer returns an empty string.
The grader checks script output, compilation and clippy -D warnings. Long
scripts and extra heap allocations may expose some memory errors, but they
do not prove unsafe code sound or detect every leak. Review the live-window
invariant and use values with observable destructors when testing ownership.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.