Skip to content

← Unsafe and Soundness step 17 of 24

Hard Framework

Extend a vector with insertion and consuming iteration

Extend the supplied MyVec<T> with insertion, removal, slice access and an iterator that takes ownership of the vector's elements.

pub fn vec2_script(ops: Vec<String>) -> Vec<String>

Implement insert, remove, Deref<Target = [T]>, DerefMut, and IntoIterator<Item = T> with an IntoIter<T> type. Support non-Copy values without cloning them. Keep the supplied allocation strategy and script interpreter. This teaching vector explicitly rejects zero-sized types in new; supporting them is outside this exercise.

Operations and output

The script starts with an empty MyVec<String> and returns one string for every command. Invalid commands or indices leave the vector unchanged except for drain_take, which always consumes it.

Command Output and effect
push S Append S; return "ok".
insert I S Insert before index I, allowing I == len; return "ok", or "err" for an invalid index.
remove I Remove and return the indexed string; return "none" if the index is invalid or I >= len.
sort Sort strings using Rust's string ordering; return "ok".
join Join current elements with commas.
len Return the number of elements as decimal text.
drain_take N Consume the vector, join up to its first N elements with a vertical bar, and discard the rest.
Unknown command Return "?".

After drain_take, the script has a fresh empty vector. Taking zero still discards every old element; taking more than the length returns all elements. Joining an empty vector produces an empty string.

The parser splits the command at its first literal ASCII space. push keeps the remainder as its string. insert splits that remainder once more into index and string. Missing string text means an empty string. Spaces within strings are preserved; input is not trimmed or normalized. sort, join and len ignore any remainder.

Indices and counts use Rust's usize parser, which accepts a leading +. Malformed or unrepresentable insert/remove indices are invalid. A malformed or unrepresentable drain_take count defaults to zero.

commands: ["push a", "push b", "insert 1 x", "remove 0", "join", "drain_take 1", "len"]
results:  ["ok",     "ok",     "ok",         "a",        "x,b",  "x",            "0"]

The public insert and remove methods themselves should panic on invalid indices before changing the vector. The interpreter checks first to produce the error strings above.

Shift values without duplicating ownership

Insertion shifts the suffix one slot right before writing the new value. Removal reads the removed value before shifting the following elements left. Use ptr::copy for these shifts: the ranges can overlap. A shift of zero or one element need not overlap, but the implementation must handle longer shifts too. Undefined behavior from a wrong copy primitive has no guaranteed symptom.

Raw copying duplicates bytes, not ownership rights. Treat the old source slots as moved from where appropriate. After removal, the old final slot is outside the initialized prefix and must not be dropped again. Keep len consistent with which values the vector still owns. The raw read/copy/write operations here do not invoke an element's Clone or Drop implementation.

See ptr::copy for its overlap and validity requirements. Retain safety comments explaining the ranges used.

Expose only initialized elements

Deref returns a shared slice of length len; DerefMut returns a mutable slice of the same length. Capacity includes spare storage that must not be exposed as initialized T values. The empty vector's dangling sentinel is non-null and aligned, as required even for an empty slice.

Shared slice methods become available through Deref when their type bounds hold. Mutating methods such as sort also require DerefMut. The script's join and sort calls exercise these two forms of access.

Transfer the allocation to IntoIter

Consuming iteration transfers responsibility for the allocation from MyVec to IntoIter. Suppress the original vector's destructor with ManuallyDrop before transferring its pointer, length and capacity. See ManuallyDrop.

Track the unyielded range separately from the allocation capacity. Each next() reads one value and advances the start; that value belongs to the caller. Exhaustion returns None on subsequent calls. Report the remaining element count in both parts of size_hint.

On iterator destruction, drop only unyielded elements, then release the buffer using its original capacity-based layout. For four stored elements in an allocation of capacity eight, taking two leaves two values to destroy but an eight-slot allocation to free. Never infer capacity from the remaining count. An iterator with zero capacity has no allocation to deallocate.

Check cleanup separately

Test front, middle and end insertion/removal, empty vectors, repeated growth, full consumption and partial consumption. A non-Clone value with a destructor counter can reveal missed or repeated destruction that string outputs miss.

The supplied cleanup loops can leak remaining resources if an element's destructor panics. They are a teaching implementation, not a complete match for standard Vec's cleanup behavior. Tests with nonpanicking destructors do not establish panic cleanup or general soundness.

The runner requires compilation, tests and Clippy with warnings denied. Its extra allocations after the script may expose memory bugs, but they do not reliably detect leaks or prove that unsafe code is correct.