We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 8 of 24
`ptr::read`, `write`, `copy`, `copy_nonoverlapping`, `drop_in_place`
pub fn dedup_in_place(v: Vec<String>) -> Vec<String>
Reimplement Vec::dedup by hand, on the raw buffer. Remove consecutive
duplicates — ["a","a","b","b","b","c"] becomes ["a","b","c"] — by walking
a read index and a write index, taking each duplicate out of the buffer and
dropping it, moving survivors down, and publishing the new length with
set_len.
No retain, no dedup, no building a second vector. The elements are
String, so ownership is real and mistakes cost something.
The starter has two bugs on one line. Clippy names the first.
Where the ownership model meets the memory model
This is the item where “a bitwise copy is not the same thing as a move” becomes a bug you can hold.
ptr::read(src) produces a bitwise copy of the value at src and
hands it to you as an owned value. It does not touch the source. So if
T: !Copy you now have two values that both believe they own the same heap
allocation, and both will run their destructor. Exactly one of them must be
forgotten, overwritten, or moved out of range before the other drops.
ptr::write(dst, val) stores val at dst without dropping whatever
was there. That is precisely what you want for an uninitialised slot and
catastrophic for an initialised one: the old String‘s heap buffer is simply
forgotten. A leak, not a crash — which is worse, because nothing tells you.
ptr::copy(src, dst, n) is memmove. Overlap is allowed.
ptr::copy_nonoverlapping(src, dst, n) is memcpy. Overlap is undefined
behaviour, and this is a nasty one: it works fine for small n and small
overlaps and corrupts silently for larger ones, so it passes your unit tests
and fails in production.
ptr::drop_in_place(p) runs T‘s destructor on a place you cannot move
out of. It is what you want when the value must die where it lies.
Every count in this API is in ELEMENTS, not bytes. C’s memcpy counts
bytes; Rust’s does not. Clippy’s size_of_in_element_count is deny-by-default
precisely because transliterating C here multiplies your overrun by
size_of::<T>().
::: question You ptr::read a String out of slot 5 and bind it to dup.
What is the state of slot 5 now?
It still contains a byte-identical String — pointer, length, capacity, all
the same — and that copy is a lie.
Nothing was zeroed. Nothing was marked. The buffer looks exactly as it did.
But ownership of the heap allocation now belongs to dup, and when dup goes
out of scope the allocation is freed. If anything later treats slot 5 as a
live String — reads it, drops it, includes it in a length — it is touching
freed memory.
This is why the bookkeeping is the whole exercise. In safe Rust the compiler
tracks which places are still owners and inserts drop flags for you. Here the
compiler has stopped tracking, and len is the only record of who is alive.
Getting len right, at the right moment, is the entire safety argument.
:::
The std::ptr validity checklist
Every safety comment in this problem should discharge these five, from the
std::ptr module docs. A pointer is valid for an access if it is:
- non-null (except for zero-sized accesses);
-
dereferenceable — the whole
size_of::<T>()byte range is inside one live allocation; -
properly aligned for
T; -
pointing at a valid value of
T(for reads); -
respecting the aliasing rules — no live
&mutelsewhere to the same place, no live&if you are writing.
“Inside one allocation” is doing real work in item 3. A pointer that reaches from one heap block into an adjacent one is invalid even if both blocks are live and the arithmetic looks fine.
Writing this one
The shape is two indices. write is where the next survivor goes; read
scans forward. Compare v[read] with the last survivor at write - 1. If
they match, that element is a duplicate: ptr::read it out and drop it. If
they differ, move it down to write and bump write.
Two decisions worth thinking about before you type.
Which copy routine? The source and destination indices satisfy
write <= read, so when write != read the single-element ranges genuinely do
not overlap and either routine would work. Use ptr::copy anyway. The reason
is that a one-element copy_nonoverlapping here is a coincidence of this
algorithm, and the moment somebody generalises the loop to move a run of k
survivors it becomes a real overlap and real UB. Prefer the routine whose
precondition you can state without appealing to the current loop bounds.
When does len change? Only at the end, with one set_len(write). In
between, v.len() still says len, which means the vector is temporarily
lying — slots you have read out are still counted. That is fine here because
nothing in the loop can panic (comparing and dropping Strings does not), so
no destructor ever observes the inconsistent state. Item 17.21 is about what
happens when that assumption is false.
::: question The starter writes
copy_nonoverlapping(p.add(read), p.add(write), size_of::<String>()).
What does that actually do?
It copies 24 Strings instead of one, off the end of the buffer, in both
directions.
size_of::<String>() is 24 on a 64-bit target — a pointer, a length and a
capacity. The count argument is in elements, so the call reads 24 String
values starting at read and writes 24 starting at write. Almost all of
those are past the end of the allocation.
Clippy catches this specific mistake by name:
error: found a count of bytes instead of a count of elements of `T`
and the lint, size_of_in_element_count, is deny-by-default, because there
is no plausible correct program that looks like this. It exists because the
mistake is so mechanical: everybody has written memcpy(dst, src, n * sizeof(T)) a thousand times, and the muscle memory transfers before the
knowledge does.
:::
What this grader cannot check
Two things, and they are opposite failures.
A leak is invisible. If you never drop the duplicates, every test here
still passes. String‘s heap buffers just accumulate. Rust considers leaking
safe — item 17.21 explains why that is a deliberate design decision rather
than an oversight — so no lint and no test will tell you. Dropping the
duplicates is required by the statement and enforced only by your own care.
A double free usually aborts, but not reliably. The 10 000-element cases are here to make heap corruption likely to surface, and the harness deliberately allocates two thousand strings after your function returns so that a damaged allocator has somewhere to fall over. That is a smoke detector, not a proof.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.