Mem::uninitialized, `!` and trap representations

See, for example, here. This is essentially a small vector, as others have mentioned. If returning uninitialized values were unsafe behavior, it would totally ruin modularity.

In general, if a field is not supposed to be used at all in unsafe code, I want to make that field uninitialized, not just for performance but for easier debugging. Valgrind will complain at uses of uninitialized values, but not at uses of "sane defaults" like 0. Additionally, it just makes code more clear: if I see that something is uninitialized, it is obvious that I shouldn't use it, but if something is 0, I don't know what will happen. Declaring it illegal to return uninitialized values makes it illegal to have a new method, which makes modularity worse and leads to more bug-ridden code: I have to separately verify every place where an object is constructed to make sure that the right fields are filled out, when a new method could do that automatically. Passing uninitialized values to functions should also be legal, especially behind pointers.

Essentially, the rules you suggest make it nearly impossible to use partially uninitialized structures in a modular way, and we ought to support those structures.


There are a few pieces of code that I definitely expect to be safe that are not under your rule:

unsafe fn my_uninit<T>() -> T {
    mem::uninitialized()
}
// This is like ptr::read, but takes a reference (so is somewhat safer),
// should compile to the same code locally (the compiler can optimize
// away writes of uninitialized values), and tells anyone reading this
// code that I won't use whatever is behind the reference anymore.
unsafe fn read_final<T>(ptr: &mut T) -> T {
    mem::replace(ptr, mem::uninitialized())
}
// This is literally the implementation of `ptr::read` in the standard
// library today. Note that it involves passing a reference to an
// uninitialized value to a function.
pub unsafe fn read<T>(src: *const T) -> T {
    let mut tmp: T = mem::uninitialized();
    copy_nonoverlapping(src, &mut tmp, 1);
    tmp
}

I agree that this should be valid, and the compiler should be permitted to lift data.data[0] out of the look as an optimization. This is actually a place where making the rules for when you can use uninitizalized data stricter hurts optimization - unless it is legal to read uninitialized data, the compiler can't do LICM properly here.


I think the rule for use of invalid data (which includes, but is not limited to uninitialized data) can be very simple: everything but inspecting the data is valid. Primitive operations on invalid values return arbitrary, possibly invalid results, and branching based on invalid data (as in, e.g., match mem::uninitialized() or if mem:uninitialized()) is undefined behavior. This

  • matches intuition, since it treats uninitialized and invalid data just like anything else.
  • has a nice parametricity property: if a function (e.g. fn foo<T>(bar: T)) is fully generic on a type, then it is valid to pass uninitialized/invalid data to that function. This can be used to be fully confident that functions like ptr::read, ptr::write, mem::replace, mem::swap, Vec::push, etc. are all safe to execute on uninitialized/invalid data.
5 Likes