Should you implement `Deref` for newtype wrappers?

Look at this code:

struct Wrapper<T> {
    value: T,
}

impl<T> Deref for Wrapper<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

Should Deref be implemented for Wrapper?
Wrapper doesn't have any associated methods but it isn't a smart pointer, it owns the value.

1 Like

If it's just for managing ownership or lifetimes, then yes.

If it's supposed to uphold some invariants or prevent mixing with other types (like the classic Celcius vs Fahrenheit wrappers), then no.

That means that a "safe" wrapper for MaybeUninit should implement Deref, correct?

MaybeUninit is special, because it doesn't know if it contains valid data. Deref is not allowed to give an invalid/uninitialized reference.

Option is a MaybeUninit with knowledge whether it's initialized. However, Deref is also supposed to be infallible, so it's not appropriate for Option either, but technically you could have a panicking Deref on an Option, it'd just be fragile and annoying.

7 Likes

Basically null.

4 Likes

If I made it safe (as in made uninit unsafe), would that work?

The point is, you have to ensure that whenever Deref::deref() fires (which is basically any time an instance of the wrapper exists), the wrapped value is valid and initialized. Otherwise, you'll have undefined behavior.

2 Likes

Uninitialized data in Rust is always considered unsafe and can't be made safe other than by initializing it.

You can lazily initialize data before returning from Deref (with care to be thread safe and reentrant), and that's what LazyCell does:

5 Likes

That's why I promise to initialize it by making the uninit method unsafe

Making a method unsafe indicates that there's a precondition you have to meet before you call it. In the case of Deref for a potentially uninitialized value (like MaybeUninit), then you need fn deref(&self) -> &Self::Target to be unsafe fn deref, because the precondition is that you need to be sure that this value is not uninit before you call deref.

But that's then not the Deref trait, but a new "UnsafeDeref" or similar trait.

This is much, much worse to work with. It's basically what we had with mem::uninitialized, where you're promising when you call it "yeah, it's horrible right now, but I promise I'll fix it later".

That just doesn't work. Critically, it's not usefully checkable: you can't check anything in the unsafe call. When it's assume_init that's unsafe, then MIRI gives you can error right there if it's not actually initialized. That's so much better than getting weird errors on something later that were caused by getting your logic wrong, but there's no nice way to tie it back to why all of a sudden your addition is UB.

3 Likes

At the high level, T should implement Deref if and only if &T should always be usable as if it were &Target, as well as the contrapositive that any API asking for &Target should accept &T, without any value of T which violates that expectation.

Furthermore, T should have no methods (that are callable with method syntax) which differ in logical semantics from the method on Target. If Target is a generic type (e.g. Box<T>: Deref<Target=T>), this essentially means T should have no methods. If T and Target are defined by the same author (e.g. String: Deref<Target=str>), then T can have its own methods, as the author can avoid creating a method name conflict. But, sneakily, if T and Target come from different crates, defining methods for T is a potential future problem if Target also decides to define a method with the same name in the future.

So, as a simple, first-order takeaway: if the wrapper is a trivial marker, then it can implement Deref. If the wrapper's entire purpose is to manage its inner type, without modifying the extant semantics of that type, it should implement Deref. If T behaves differently than Target when Target would compile with that usage, it shouldn't implement Deref.

And a second-order takeaway: if ownership semantics would allow you to implement DerefMut but other concerns make doing so questionable, you usually shouldn't implement Deref. (There are of course exceptions, e.g. Lazy* types.)

5 Likes

There's just no other way to have global state in Rust. You either have to promise to initialize it or have a Lazy. Pick your poison dude

The Lazy* types don't have an unsafe uninit though. They have safe construction, and keep track of whether they've been initialized. The Lazy* types by holding a function to automatically call on first access, the Once* types by having you provide an initialization function on access.

If you have an unsafe initialization function which puts your type into an unsafe state where safe manipulation can result in UB (e.g. read uninitialized memory), that's a poor API.

As much as reasonable, unsafe should only impose precondition restrictions on the caller. APIs like Pin::get_mut_unchecked or str::as_bytes_mut that restrict what you're allowed to do with the returned value are sometimes necessary, but are always much worse to work with than precondition-only unsafe.

And it's even worse when Deref can detonate the UB, since deref is called implicitly a lot in Rust syntax.

3 Likes