Skip to content

← Unsafe and Soundness step 8 of 24

Medium Primitives

ptr::read, write, copy, copy_nonoverlapping, drop_in_place

Implement dedup_in_place(v: Vec<String>) -> Vec<String> using the vector's raw buffer. Keep the first string in each run of consecutive equal strings.

For ["a", "a", "b", "b", "a"], return ["a", "b", "a"]. Equal strings separated by a different string remain separate. Preserve order and handle empty input, a single element and empty strings.

Do not call retain or dedup, build a second vector, or clone the survivors. Move survivors within the existing buffer and drop each removed string exactly once. String equality compares contents; the strings need not share an allocation.

Track the live elements

Use a read index to scan forward and a write index for the next survivor. With nonempty input, keep the first element and start both indices at one.

Before processing each element:

  • Slots 0..write hold the survivors so far.
  • Slots write..read are no longer independent owners. Their old values were either dropped or moved into the survivor prefix.
  • Slots read..len still hold unprocessed strings.

Compare the current string with the last survivor at write - 1. If equal, remove and drop the current string. Otherwise, move it to write if the indices differ, then increment write.

Do not compare against read - 1: that slot may already have been removed or moved. When the scan finishes, use set_len(write) to make the vector's recorded length match its live prefix.

What the pointer operations do

Operation Effect Ownership responsibility
ptr::read(src) Returns a bitwise copy without changing the source bytes For String, stop treating the source as a separate owner
ptr::write(dst, value) Stores a value without dropping the destination Drop or move any previous owner first if it must be released
ptr::copy(src, dst, count) Copies count elements; overlap is allowed It does not clone values or manage their destructors
ptr::copy_nonoverlapping(src, dst, count) Copies count elements; ranges must not overlap The same ownership rules apply
ptr::drop_in_place(ptr) Destroys the value at that location Do not later use or drop that value again

The two copy functions count elements, not bytes. read, write and drop_in_place operate on one value and have no count parameter.

Reading a String with ptr::read does not clear its old slot. You can drop the returned String, but the original slot must then remain outside the live set until it is overwritten. Copying a survivor similarly leaves stale source bytes that must not be dropped as another owner.

This bookkeeping does not replace the other unsafe requirements. Reads must use live, initialized strings; source and destination ranges must remain within the vector allocation and meet the operation's alignment requirements. Temporary comparison references must end before conflicting writes. Even a zero-element copy still requires properly aligned pointers.

The starter's defects

The copy count is size_of::<String>(), so the starter copies that many String elements, not one String. On a typical 64-bit target that means 24 elements. The actual size is target-dependent. This can exceed the valid source or destination range and triggers Clippy's size_of_in_element_count lint.

The starter also calls copy_nonoverlapping when write == read. For a nonzero count, identical ranges overlap. Skip the copy when an element is already in the right slot.

When write < read, a one-element copy between these distinct String slots does not overlap. Either copy function can be correct under that condition. Recheck the range argument if you later change the algorithm to move several elements at once; do not assume that distinct starting addresses imply disjoint ranges.

Finally, the duplicate branch does not drop the removed strings. Reducing the vector's length will not drop them for you, so this leaks their buffers.

Why the final length update needs care

During compaction, the vector's old length includes slots that are no longer live owners. Unwinding through that state could make Vec drop stale values. This concrete loop relies on String comparison and destruction not unwinding, and on doing no other fallible work before restoring the length.

Do not generalize it to arbitrary types, callbacks or fallible operations without a cleanup strategy. A guard may need to repair the vector during unwinding. The leak-amplification lesson examines ownership bookkeeping when control flow changes unexpectedly.

What passing tests establishes

The harness checks the output strings, length and a digest. It also allocates 2,000 strings afterward, which may expose some heap damage. That extra allocation is not a reliable detector of undefined behavior.

Output tests alone do not establish that duplicates were freed or that the unsafe implementation is sound. Memory-checking tools and an argument for each unsafe operation provide additional evidence. Correct length bookkeeping is necessary, but bounds, initialization, alignment and aliasing also matter.

The submission must compile, match the output cases and pass Clippy with warnings denied.