Skip to content

Commit 5f6e0ab

Browse files
committed
clean up vec chapter of tarpl
1 parent 42c2f10 commit 5f6e0ab

File tree

9 files changed

+192
-177
lines changed

9 files changed

+192
-177
lines changed

src/doc/tarpl/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* [Deref](vec-deref.md)
4747
* [Insert and Remove](vec-insert-remove.md)
4848
* [IntoIter](vec-into-iter.md)
49+
* [RawVec](vec-raw.md)
4950
* [Drain](vec-drain.md)
5051
* [Handling Zero-Sized Types](vec-zsts.md)
5152
* [Final Code](vec-final.md)

src/doc/tarpl/vec-alloc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Okay, now we can write growing. Roughly, we want to have this logic:
4646
if cap == 0:
4747
allocate()
4848
cap = 1
49-
else
49+
else:
5050
reallocate
5151
cap *= 2
5252
```

src/doc/tarpl/vec-dealloc.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
Next we should implement Drop so that we don't massively leak tons of resources.
44
The easiest way is to just call `pop` until it yields None, and then deallocate
5-
our buffer. Note that calling `pop` is uneeded if `T: !Drop`. In theory we can
6-
ask Rust if T needs_drop and omit the calls to `pop`. However in practice LLVM
7-
is *really* good at removing simple side-effect free code like this, so I wouldn't
8-
bother unless you notice it's not being stripped (in this case it is).
5+
our buffer. Note that calling `pop` is unneeded if `T: !Drop`. In theory we can
6+
ask Rust if `T` `needs_drop` and omit the calls to `pop`. However in practice
7+
LLVM is *really* good at removing simple side-effect free code like this, so I
8+
wouldn't bother unless you notice it's not being stripped (in this case it is).
99

10-
We must not call `heap::deallocate` when `self.cap == 0`, as in this case we haven't
11-
actually allocated any memory.
10+
We must not call `heap::deallocate` when `self.cap == 0`, as in this case we
11+
haven't actually allocated any memory.
1212

1313

1414
```rust,ignore

src/doc/tarpl/vec-deref.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
% Deref
22

3-
Alright! We've got a decent minimal ArrayStack implemented. We can push, we can
4-
pop, and we can clean up after ourselves. However there's a whole mess of functionality
5-
we'd reasonably want. In particular, we have a proper array, but none of the slice
6-
functionality. That's actually pretty easy to solve: we can implement `Deref<Target=[T]>`.
7-
This will magically make our Vec coerce to and behave like a slice in all sorts of
8-
conditions.
3+
Alright! We've got a decent minimal stack implemented. We can push, we can
4+
pop, and we can clean up after ourselves. However there's a whole mess of
5+
functionality we'd reasonably want. In particular, we have a proper array, but
6+
none of the slice functionality. That's actually pretty easy to solve: we can
7+
implement `Deref<Target=[T]>`. This will magically make our Vec coerce to, and
8+
behave like, a slice in all sorts of conditions.
99

10-
All we need is `slice::from_raw_parts`.
10+
All we need is `slice::from_raw_parts`. It will correctly handle empty slices
11+
for us. Later once we set up zero-sized type support it will also Just Work
12+
for those too.
1113

1214
```rust,ignore
1315
use std::ops::Deref;
@@ -36,5 +38,5 @@ impl<T> DerefMut for Vec<T> {
3638
}
3739
```
3840

39-
Now we have `len`, `first`, `last`, indexing, slicing, sorting, `iter`, `iter_mut`,
40-
and all other sorts of bells and whistles provided by slice. Sweet!
41+
Now we have `len`, `first`, `last`, indexing, slicing, sorting, `iter`,
42+
`iter_mut`, and all other sorts of bells and whistles provided by slice. Sweet!

src/doc/tarpl/vec-drain.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Let's move on to Drain. Drain is largely the same as IntoIter, except that
44
instead of consuming the Vec, it borrows the Vec and leaves its allocation
5-
free. For now we'll only implement the "basic" full-range version.
5+
untouched. For now we'll only implement the "basic" full-range version.
66

77
```rust,ignore
88
use std::marker::PhantomData;
@@ -38,6 +38,9 @@ impl<T> RawValIter<T> {
3838
RawValIter {
3939
start: slice.as_ptr(),
4040
end: if slice.len() == 0 {
41+
// if `len = 0`, then this is not actually allocated memory.
42+
// Need to avoid offsetting because that will give wrong
43+
// information to LLVM via GEP.
4144
slice.as_ptr()
4245
} else {
4346
slice.as_ptr().offset(slice.len() as isize)
@@ -137,5 +140,7 @@ impl<T> Vec<T> {
137140
}
138141
```
139142

143+
For more details on the `mem::forget` problem, see the
144+
[section on leaks][leaks].
140145

141-
146+
[leaks]: leaking.html

src/doc/tarpl/vec-insert-remove.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
% Insert and Remove
22

3-
Something *not* provided but slice is `insert` and `remove`, so let's do those next.
3+
Something *not* provided by slice is `insert` and `remove`, so let's do those
4+
next.
45

56
Insert needs to shift all the elements at the target index to the right by one.
67
To do this we need to use `ptr::copy`, which is our version of C's `memmove`.
7-
This copies some chunk of memory from one location to another, correctly handling
8-
the case where the source and destination overlap (which will definitely happen
9-
here).
8+
This copies some chunk of memory from one location to another, correctly
9+
handling the case where the source and destination overlap (which will
10+
definitely happen here).
1011

1112
If we insert at index `i`, we want to shift the `[i .. len]` to `[i+1 .. len+1]`
1213
using the *old* len.

src/doc/tarpl/vec-into-iter.md

Lines changed: 14 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,20 @@ allocation.
1111
IntoIter needs to be DoubleEnded as well, to enable reading from both ends.
1212
Reading from the back could just be implemented as calling `pop`, but reading
1313
from the front is harder. We could call `remove(0)` but that would be insanely
14-
expensive. Instead we're going to just use ptr::read to copy values out of either
15-
end of the Vec without mutating the buffer at all.
14+
expensive. Instead we're going to just use ptr::read to copy values out of
15+
either end of the Vec without mutating the buffer at all.
1616

1717
To do this we're going to use a very common C idiom for array iteration. We'll
18-
make two pointers; one that points to the start of the array, and one that points
19-
to one-element past the end. When we want an element from one end, we'll read out
20-
the value pointed to at that end and move the pointer over by one. When the two
21-
pointers are equal, we know we're done.
18+
make two pointers; one that points to the start of the array, and one that
19+
points to one-element past the end. When we want an element from one end, we'll
20+
read out the value pointed to at that end and move the pointer over by one. When
21+
the two pointers are equal, we know we're done.
2222

2323
Note that the order of read and offset are reversed for `next` and `next_back`
2424
For `next_back` the pointer is always *after* the element it wants to read next,
2525
while for `next` the pointer is always *at* the element it wants to read next.
26-
To see why this is, consider the case where every element but one has been yielded.
26+
To see why this is, consider the case where every element but one has been
27+
yielded.
2728

2829
The array looks like this:
2930

@@ -35,6 +36,10 @@ The array looks like this:
3536
If E pointed directly at the element it wanted to yield next, it would be
3637
indistinguishable from the case where there are no more elements to yield.
3738

39+
Although we don't actually care about it during iteration, we also need to hold
40+
onto the Vec's allocation information in order to free it once IntoIter is
41+
dropped.
42+
3843
So we're going to use the following struct:
3944

4045
```rust,ignore
@@ -46,8 +51,8 @@ struct IntoIter<T> {
4651
}
4752
```
4853

49-
One last subtle detail: if our Vec is empty, we want to produce an empty iterator.
50-
This will actually technically fall out doing the naive thing of:
54+
One last subtle detail: if our Vec is empty, we want to produce an empty
55+
iterator. This will actually technically fall out doing the naive thing of:
5156

5257
```text
5358
start = ptr
@@ -155,139 +160,3 @@ impl<T> Drop for IntoIter<T> {
155160
}
156161
}
157162
```
158-
159-
We've actually reached an interesting situation here: we've duplicated the logic
160-
for specifying a buffer and freeing its memory. Now that we've implemented it and
161-
identified *actual* logic duplication, this is a good time to perform some logic
162-
compression.
163-
164-
We're going to abstract out the `(ptr, cap)` pair and give them the logic for
165-
allocating, growing, and freeing:
166-
167-
```rust,ignore
168-
169-
struct RawVec<T> {
170-
ptr: Unique<T>,
171-
cap: usize,
172-
}
173-
174-
impl<T> RawVec<T> {
175-
fn new() -> Self {
176-
assert!(mem::size_of::<T>() != 0, "TODO: implement ZST support");
177-
unsafe {
178-
RawVec { ptr: Unique::new(heap::EMPTY as *mut T), cap: 0 }
179-
}
180-
}
181-
182-
// unchanged from Vec
183-
fn grow(&mut self) {
184-
unsafe {
185-
let align = mem::align_of::<T>();
186-
let elem_size = mem::size_of::<T>();
187-
188-
let (new_cap, ptr) = if self.cap == 0 {
189-
let ptr = heap::allocate(elem_size, align);
190-
(1, ptr)
191-
} else {
192-
let new_cap = 2 * self.cap;
193-
let ptr = heap::reallocate(*self.ptr as *mut _,
194-
self.cap * elem_size,
195-
new_cap * elem_size,
196-
align);
197-
(new_cap, ptr)
198-
};
199-
200-
// If allocate or reallocate fail, we'll get `null` back
201-
if ptr.is_null() { oom() }
202-
203-
self.ptr = Unique::new(ptr as *mut _);
204-
self.cap = new_cap;
205-
}
206-
}
207-
}
208-
209-
210-
impl<T> Drop for RawVec<T> {
211-
fn drop(&mut self) {
212-
if self.cap != 0 {
213-
let align = mem::align_of::<T>();
214-
let elem_size = mem::size_of::<T>();
215-
let num_bytes = elem_size * self.cap;
216-
unsafe {
217-
heap::deallocate(*self.ptr as *mut _, num_bytes, align);
218-
}
219-
}
220-
}
221-
}
222-
```
223-
224-
And change vec as follows:
225-
226-
```rust,ignore
227-
pub struct Vec<T> {
228-
buf: RawVec<T>,
229-
len: usize,
230-
}
231-
232-
impl<T> Vec<T> {
233-
fn ptr(&self) -> *mut T { *self.buf.ptr }
234-
235-
fn cap(&self) -> usize { self.buf.cap }
236-
237-
pub fn new() -> Self {
238-
Vec { buf: RawVec::new(), len: 0 }
239-
}
240-
241-
// push/pop/insert/remove largely unchanged:
242-
// * `self.ptr -> self.ptr()`
243-
// * `self.cap -> self.cap()`
244-
// * `self.grow -> self.buf.grow()`
245-
}
246-
247-
impl<T> Drop for Vec<T> {
248-
fn drop(&mut self) {
249-
while let Some(_) = self.pop() {}
250-
// deallocation is handled by RawVec
251-
}
252-
}
253-
```
254-
255-
And finally we can really simplify IntoIter:
256-
257-
```rust,ignore
258-
struct IntoIter<T> {
259-
_buf: RawVec<T>, // we don't actually care about this. Just need it to live.
260-
start: *const T,
261-
end: *const T,
262-
}
263-
264-
// next and next_back literally unchanged since they never referred to the buf
265-
266-
impl<T> Drop for IntoIter<T> {
267-
fn drop(&mut self) {
268-
// only need to ensure all our elements are read;
269-
// buffer will clean itself up afterwards.
270-
for _ in &mut *self {}
271-
}
272-
}
273-
274-
impl<T> Vec<T> {
275-
pub fn into_iter(self) -> IntoIter<T> {
276-
unsafe {
277-
// need to use ptr::read to unsafely move the buf out since it's
278-
// not Copy.
279-
let buf = ptr::read(&self.buf);
280-
let len = self.len;
281-
mem::forget(self);
282-
283-
IntoIter {
284-
start: *buf.ptr,
285-
end: buf.ptr.offset(len as isize),
286-
_buf: buf,
287-
}
288-
}
289-
}
290-
}
291-
```
292-
293-
Much better.

src/doc/tarpl/vec-layout.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ pub struct Vec<T> {
1313
# fn main() {}
1414
```
1515

16-
And indeed this would compile. Unfortunately, it would be incorrect. The compiler
17-
will give us too strict variance, so e.g. an `&Vec<&'static str>` couldn't be used
18-
where an `&Vec<&'a str>` was expected. More importantly, it will give incorrect
19-
ownership information to dropck, as it will conservatively assume we don't own
20-
any values of type `T`. See [the chapter on ownership and lifetimes]
21-
(lifetimes.html) for details.
16+
And indeed this would compile. Unfortunately, it would be incorrect. The
17+
compiler will give us too strict variance, so e.g. an `&Vec<&'static str>`
18+
couldn't be used where an `&Vec<&'a str>` was expected. More importantly, it
19+
will give incorrect ownership information to dropck, as it will conservatively
20+
assume we don't own any values of type `T`. See [the chapter on ownership and
21+
lifetimes] (lifetimes.html) for details.
2222

23-
As we saw in the lifetimes chapter, we should use `Unique<T>` in place of `*mut T`
24-
when we have a raw pointer to an allocation we own:
23+
As we saw in the lifetimes chapter, we should use `Unique<T>` in place of
24+
`*mut T` when we have a raw pointer to an allocation we own:
2525

2626

2727
```rust
@@ -40,9 +40,10 @@ pub struct Vec<T> {
4040

4141
As a recap, Unique is a wrapper around a raw pointer that declares that:
4242

43-
* We own at least one value of type `T`
43+
* We may own a value of type `T`
4444
* We are Send/Sync iff `T` is Send/Sync
45-
* Our pointer is never null (and therefore `Option<Vec>` is null-pointer-optimized)
45+
* Our pointer is never null (and therefore `Option<Vec>` is
46+
null-pointer-optimized)
4647

4748
That last point is subtle. First, it makes `Unique::new` unsafe to call, because
4849
putting `null` inside of it is Undefined Behaviour. It also throws a

0 commit comments

Comments
 (0)