?Uninit types?

uh, anyone remembers a proposal along the lines of:

pub struct Foo {
  a: Bar,
  pub b: Baz,
}

impl ?Uninit for Foo {}

fn partially_init_foo(foo: &mut Foo(!a) -> Foo(a)) {
  foo.a = something;
}

fn partially_uninit_foo(foo: &mut Foo(a) -> Foo(!a)) {
  drop(foo.a);
}

fn those_bangs_can_be_inferred(foo: &mut Foo(!a) -> Foo(a)) {
  foo.a = something;
}

//pub fn no_private_in_public(foo: &mut Foo(a) -> Foo(!a)) { drop(foo.a) } // ERROR: no private in public

pub fn partially_init_public(foo: &mut Foo() -> Foo(b)) {
  foo.b = something;
  //foo.a // ERROR: a may be uninitialized
}

pub fn init_everything(foo: &mut Foo() -> Foo) {
  foo.a = something;
  foo.b = something_else;
}

//impl Drop for Foo(a) {...} // ERROR: no private in public

impl Drop for Foo(b) {
  fn drop(&mut self) {
    // perfectly safe
  }
}

let x = std::mem::uninitialized::<Foo()>(); // safe
//let x = std::mem::uninitialized::<Foo>(); // ERROR: unsafe
let x = unsafe { std::mem::uninitialized::<Foo>() }; // UB

You mean this?

1 Like

Yeah! Hm, why did that discussion die down... :‌(

Probably because the design was really complex, and didn't carry its weight. To be fair, the discussion that spawned it off (my RFC) was also really complex and didn't carry its weight. My views on the topic have changed significantly, and I don't think we need uninitialized memory to be so accessible.

So, for me, there is a significant lack of motivation for either your pre-RFC or my RFC. You can do many of the things in both RFCs already, and you can do field-by-field initialization with the currently nightly raw_ref_op in conjunction with MaybeUninit. This is powerful enough for most use-cases.

4 Likes

Well...

Our design/proposal is meant to be 100% safe Rust. It even involves making mem::uninitialized and mem::zeroed - two functions that are currently straight-up UB - safe. (if specialization ever lands - and so far it doesn't look like it should, as it seems like it'd make adding trait impls a breaking change and we don't want that. altho note that mem::zeroed would do the same as mem::uninitialized due to how our proposal is meant to work. but anyway...)

What's the best way to go about making a list of reasons for why we should have ?Uninit types? (e.g.: safe placement new, etc)

Now I haven't been involved in the discussion before, but I think you'll need to address modern Rust solutions (MaybeUninit<T>) and how this compares with that, and how it complements and improves the situation - what it does that is useful, that we can't already do.

2 Likes

The main one would be 100% safe "placement new" with no special syntax. In particular, something along the lines of uh...

fn new_at(self: &mut Self() -> Self) {
  *self = Self::new();
}

There are other benefits. Not sure how to go about it tho.

(Note: leaking is safe, so in many cases it'd be necessary to use closures instead of returning &mut. In particular, Vec.push_with would have to take a closure instead of returning a PIT reference, or else panics and leaks can get in the way of the initialization and cause the Vec to drop uninitialized memory. In safe code however, panics and leaks would never be an issue.)

There is a presently unstable MaybeUninit::write, which can provide this functionality. (Although it doesn't generalize to field-by-field initialization, but that can be handled in other ways). Note: it gives you a mutable reference to the initialized value.

1 Like

MaybeUninit can't be easily extended to field-by-field initialization of enums, at least.

That's seems to be partially due to the, intentionally restricted, design of ptr::raw_const!. It does not make it possible to create a raw pointer to the fields of an enum variant. Those can only be created by matching the enum variant, which requires a filled-in discriminant (1) and will result in a reference (2).

(1) would be 'easily' solved by providing some way to write the variant alone, e.g. consider:

impl<T> *mut T {
    /// Overwrite the discriminant value.
    /// Requires that `T` is an `enum` and may set any bytes of `T` to an uninitialized state.
    pub unsafe fn write_discriminant(self, _: Discriminant<T>);
}

(2) would be solved by adding some raw-pointer operator to matching such that no intermediate reference is created. Shameless plug to RFC, it was postponed. I feel like the names I had chosen might be improved though, with some hindsight on the naming of the macros.

enum FooBar {
    Foo(usize),
    Bar(usize),
}

unsafe { // Unsafe, as it requires the discriminant to be initialized.
    // TODO: or should it? It was an unanswered part of the RFC how to best
    // differentiate here.
    match *ptr_to_foo_bar {
        FooBar::Foo(raw mut foo) => {},
        FooBar::Bar(raw mut bar) => {},
    }
}

With both in place, we can use a similar strategy as initialization of wrapper structs. First initialize the variant kind, then get pointers to the remaining parts and fill in the rest recursively.

PS: However, I also agree with the assessment of lang-team that we should first gather some use of the macro before jumping to extending it for enums specifically. Even though a consistency in treatment of types from the type system, not treating enums as an afterthought or second class, would also have some value.

Hm... We don't understand why this should be a library type instead of a language feature tho. We personally associate partial (de)initialization with something that should be in the language.

Also this:

fn doesnt_compile() {
  let mut x;
  let _ = catch_panic(AssertUnwindSafe(|| {
    x = 1;
  });
}

fn compiles_but_is_error_prone() {
  let mut x = MaybeUninit::uninit();
  let _ = catch_panic(AssertUnwindSafe(|| {
    x::write(1);
  });
}

?Uninit would maintain the properties of doesnt_compile. There's currently no way to do that with std types. It also doesn't allow partial deinitialization so if you partially initialize a MaybeUninit and get a panic you have no way to deinitialize stuff, and we believe there are safe ways to do this once you have syntax for it.