The overly short version of it is that you're not expected to be using raw allocation in Rust. You're expected to use Box
or Vec
or some other higher level container instead. The GlobalAlloc
and allocation free functions were thus designed not to be used, but to fit the specific need of swapping out the allocation implementation underlying Box
et. al.
That said, certain things do require doing manual allocation; I've written a decent amount of such code to enable some VLA use cases on stable Rust.
When you call realloc
, it's semantically equivalent to allocating a new pointer and freeing the old one. If the realloc succeeds, then all accesses must go through the new pointer, not the old one. This is the case even if the address doesn't change.
The unstable Allocator
trait used to have a grow_in_place
method which would fail if a data relocation were to take place. It got removed because despite being guaranteed "in place" it still necessarily invalidated all old pointers.
This is the case even in C.
The original pointer ptr is invalidated and any access to it is undefined behavior (even if reallocation was in-place). [cppreference]
I agree this is suboptimal wording. "This allocator" does refer to the impl GlobalAlloc
, but "ownership [...] has been transferred to this allocator" is at best vague. The next line of
The memory may or may not have been freed, and should be considered unusable.
helps, but I think it's probably better for the trait user if the trait docs would just state that the old pointer is invalidated/unusable.
What's being said is that the pointer no longer owns the memory block it pointed to, and the allocator has reclaimed ownership of that memory. This is I think a casualty of trait docs being written for two audiences: both for the consumer of the trait and the implementor of the trait.
When I'm in a project involving a decent amount of manual allocation, I'll typically write a couple of simple wrappers, roughly:
fn alloc(layout: Layout) -> NonNull<u8> {
NonNull::new(std::alloc::alloc(layout))
.unwrap_or_else(handle_alloc_error)
}
unsafe fn dealloc(ptr: NonNull<u8>, layout: Layout) {
std::alloc::dealloc(ptr.as_ptr(), layout);
}
but sometimes also, when it's less dynamic,
fn alloc<T>() -> NonNull<u8> {
let layout = Layout::new::<T>();
previous::alloc(layout)
}
unsafe fn dealloc<T>(ptr: NonNull<T>) {
let layout = Layout::new::<T>();
previous::dealloc(ptr, layout);
}
but always, if I can get away with it, it's better to stick to using Box
(or Rc
, or another container type) to manage allocation, because you get unwind cleanup for free.
When you have pointer-soup ownership, you do need to use raw pointers throughout to avoid asserting Rust's strong borrowing semantics where not intended. But even if you're going to drop to raw pointers everywhere, it's still a good idea to start out with Box
or whatever initially rather than raw allocation if you can do so.
If you're only dealing with Sized
-typed allocation, the only reason to bypass Box
is
- if
Box::new(MaybeUninit::uninit())
is failing to elide the copy / is blowing the stack, or
- if pointer directionality is cyclic and can't coexist with a
Box
owner of the root.
(Though in the latter case do consider if cyclic weak reference counting is sufficient, along with Arc::new_cyclic
designed for patterns like C++ enabled_shared_from_this
where you hold a Weak<Self>
member. In the former case, consider if Vec::with_capacity
will work; you can turn Vec<T>
into Box<[T; N]>
without reallocating if .cap()
==.len()
==N
. In either case you can get the benefit of automatically correct unwind cleanup, which is entirely manual for manual allocation.)