Determine if type is Sized through a const fn

Hi all! Working on a crate that uses LMDB as a key-value store (also planned to become open-source eventually), I have the following (unsafe) trait:

/// Types that can be stored
pub unsafe trait Storable: Ord {
    /// Is type fixed in size? Must be set to true if (and only if) type is Sized
    const FIXED_SIZE: bool;
    /// Is type equivalent to [`c_uint`] or [`c_size_t`]?
    const OPTIMIZE_INT: bool = false;
    /// Is [`Ord`] consistent with lexicographical sorting of binary representation?
    const TRIVIAL_CMP: bool = false;
    /// Pointer to aligned version of Self
    type AlignedRef<'a>: Deref<Target = Self>;
    /// Pointer to byte representation
    type BytesRef<'a>: Deref<Target = [u8]>
    where
        Self: 'a;
    /// Converts to byte slice
    fn to_bytes(&self) -> Self::BytesRef<'_>;
    /// Converts from byte slice
    unsafe fn from_bytes_unchecked(bytes: &[u8]) -> Self::AlignedRef<'_>;
    /// Compares byte representation
    unsafe fn cmp_bytes_unchecked(a: &[u8], b: &[u8]) -> cmp::Ordering {
        Self::from_bytes_unchecked(a).cmp(&Self::from_bytes_unchecked(b))
    }
}

I currently have to manually set the FIXED_SIZE constant for every (unsafe) implementation of the trait, according to whether the type has a fixed size or not.

The constant is later used here:

#[derive(Clone, Copy, Debug)]
pub struct DbOptions<K: ?Sized, V: ?Sized, C> {
    key: PhantomData<K>,
    value: PhantomData<V>,
    constraint: PhantomData<C>,
    lmdb_flags: LmdbFlags,
}

impl<K: ?Sized, V: ?Sized, C> DbOptions<K, V, C> {
    /// Set stored value type
    pub const unsafe fn value_type<T>(mut self) -> DbOptions<K, T, C>
    where
        T: ?Sized + Storable,
    {
        flag_set!(self, MDB_DUPFIXED, T::FIXED_SIZE); // <- using it here!!
        flag_set!(self, MDB_INTEGERDUP, T::OPTIMIZE_INT);
        DbOptions {
            value: PhantomData,
            ..self
        }
    }
    /* … */
}

Now I wonder if it's possible to automatically set that constant or use a const fn to evaluate whether the type is sized or not.

We already have std::mem::size_of, which returns the size (but it's only defined for Sized types). What I'm missing is something like

const fn try_size_of<T: ?Sized>() -> Option<usize>;

which would return None for types that are !Sized. Is it possible to write such a (const) function?

Then I could write:

flag_set!(self, MDB_DUPFIXED, try_size_of::<T>.is_some());

I guess a reasonable (as in "likely not to break anytime soon") hack which you can build upon would be

const fn is_sized<T: ?Sized>() -> bool {
    size_of::<&T>() == size_of::<&()>()
}
1 Like

Very nice idea, but a bit scary. If I understand it right, this relies on wide pointers consuming more memory than thin pointers. So it should hold. But… something feels dangerous about it. Would this really be true on all future platforms? Perhaps, perhaps not. I'd feel better if such a function was provided by std. But thanks for this hack anyway, I'll consider it.

Hence, "hack".

Sorry, I can't update std for you. However, you could open an RFC for adding and exposing the pertinent compiler intrinsic.

I don't see how this could not hold reasonably, since:

  • Wide pointers are a technical necessity; the size/vtable/etc. information is simply required for constructing a &!Sized; but
  • Having every pointer consume as much space as a wide pointer would be wasteful, so almost surely completely off the table in a language that aims for maximal resource efficiency.

The bigger problem is that it's not, as far as I can tell, possible to express try_size_of as a proper, const generic function, since the ?Sized bound implies that one can't call size_of::<T>() even if it's dynamically Sized. One could then reach for the next best tool, size_of_val() or size_of_val_raw(), and make up an ad-hoc value, but 1. they are not const-ready, and 2. it's not obvious how to do that generically and soundly (ptr::null() and NonNull::dangling() both require T: Sized, and so does Default::default).

That doesn't work for extern { type Foo; }. It is currently unstable though.

1 Like

Yes. extern type is highly problematic IMO for other reasons, too… I did not even try supporting them.

Since you're already relying on nightly, you could consider using something like:

#![feature(specialization)]

trait Sizedness {
    const FIXED_SIZE: bool;
}

impl<T : ?Sized> Sizedness for T {
    default
    const FIXED_SIZE: bool = false;
}

impl<T> Sizedness for T {
    const FIXED_SIZE: bool = true;
}

Now, it does rely on the incomplete specialization feature; that being said this specific specialization pattern is not a problematic one (specialization over Sized is fine).

Another approach would be to simply skip that middle default impl, and require people to manually implement it for the rare occasion where they're dealing with an unsized type:

trait Sizedness {
    const IS_IT: bool;
}
impl<T> Sizedness for T {
    const FIXED_SIZE: bool = true;
}

/// etc. (this is what specialization was providing)
impl Sizedness for str {
    const FIXED_SIZE: bool = false;
}
impl<T> Sizedness for [T] {
    const FIXED_SIZE: bool = false;
}
impl Sizedness for dyn Any + '_ {
    const FIXED_SIZE: bool = false;
}

Since unsized types don't come up that often, having that generic impl for all sized types already does 90% of the job

1 Like

Not sure if I overlook this issue well enough to open an RFC. I'm also unsure whether my need is too exotic to deserve an addition to std. I just thought maybe there was something existent in std which I was missing.

However, if you (or someone else) thinks it's nice to have try_size_of in std::mem, I'd be happy about such an RFC or feature. But it's not urgent enough for me to propose it right now.

I came up with the following (before @Yandros posted):

trait StorableChecked: Storable {
    const FIXED_SIZE_CHECKED: bool = {
        assert!(Self::FIXED_SIZE == (size_of::<&Self>() == size_of::<&()>()));
        Self::FIXED_SIZE
    };
}
impl<T: ?Sized + Storable> StorableChecked for T {}

But then I must use StorableChecked::FIXED_SIZE_CHECKED instead of Storable::FIXEDSIZE later in my code, because otherwise the assert! would not panic if the constant isn't used.

Now to @Yandros' post:

:flushed:

I didn't know that was possible. Apparently #[feature(specialization)] has a lot of open issues yet, but I'll consider using that too.

The problem with manually implementing Sizedness for unsized types is that the implementation can be wrong (and then bad stuff happens in my code). Though, admittingly, my Storable trait is marked unsafe anyway because it also provides other constants that must be set correctly when implementing the trait.

Right now, I might use @H2CO3's hack but go with:

assert!(Self::FIXED_SIZE == (size_of::<&Self>() == size_of::<&()>()));

That way, if things change in future or on other platforms or with new unstable kinds of types, I will at least get an error message during compilation and not run into bad problems at runtime. I'll still have to define FIXED_SIZE manually in each implementation of Storable, but if I make a mistake there, a compile-time error will be thrown.

Or I just skip all these checks and rely on the programmer to not make a mistake when implementing an unsafe trait. Not sure yet.

2 Likes

Inspired by your solution for overlapping implementations using with_negative_coherence, I revisited this thread and came up with the following:

#![feature(negative_impls, with_negative_coherence)]

pub mod sz {
    
    pub const fn is_sized<T: ?Sized + Sizedness>() -> bool {
        T::FIXED_SIZE
    }
    
    pub trait Sizedness: KnowsSizedness {}
    
    pub trait SizednessSized: Sizedness + Sized {}
    impl<T: Sizedness> SizednessSized for T {}
    
    pub trait SizednessUnsized: Sizedness {}
    
    impl<T: ?Sized + SizednessSized> !SizednessUnsized for T {}
    impl<T: ?Sized + SizednessUnsized> !SizednessSized for T {}

    pub trait KnowsSizedness {
        const FIXED_SIZE: bool;
    }
    
    impl<T: ?Sized + SizednessSized> KnowsSizedness for T {
        const FIXED_SIZE: bool = true;
    }
    impl<T: ?Sized + SizednessUnsized> KnowsSizedness for T {
        const FIXED_SIZE: bool = false;
    }

}

impl sz::Sizedness for i32 {}

impl sz::Sizedness for str {}
impl sz::SizednessUnsized for str {} // compiler enforces this to be added

fn main() {
    println!("Is i32 sized? {}", sz::is_sized::<i32>());
    println!("Is str sized? {}", sz::is_sized::<str>());
}

(Playground)

Output:

Is i32 sized? true
Is str sized? false

Now that's a lot of ?Sized, Sizeness, Sized and unsized :face_with_spiral_eyes:, but it seems to work :grin:.

Ah yeah, I guess that takes care of the "churn / error-prone-ness" of having to specify FIXED_SIZE = false, nice :+1:

  • (For reference, for those who may not be able to afford nightly, my previous suggestion could be made more ergonomic by replacing the manual impls with a macro)

Indeed! :smile:

I seem to have been able to reduce it a bit, though:

#![feature(negative_impls, with_negative_coherence)]

trait Sizedness {
    const FIXED_SIZE: bool;
}

trait Unsized {}
impl<T /* : Sized */> !Unsized for T {}

impl<T : ?Sized + Unsized> Sizedness for T {
    const FIXED_SIZE: bool = false;
}
impl<T /* : Sized */> Sizedness for T {
    const FIXED_SIZE: bool = true;
}

impl Unsized for str {}
impl<T> Unsized for [T] {}
impl Unsized for dyn ::core::any::Any + '_ {}
3 Likes

Is this exhaustive, i.e. does it cover all (stable) types that can be !Sized?

It seems like it's possible to hide the implementation details in a private module:

#![feature(negative_impls, with_negative_coherence)]

mod private {
    
    pub const fn is_sized<T: ?Sized + Sizedness>() -> bool {
        T::FIXED_SIZE
    }
    
    pub trait Sizedness {
        const FIXED_SIZE: bool;
    }
    
    pub trait Unsized {}
    impl<T /* : Sized */> !Unsized for T {}
    
    impl<T : ?Sized + Unsized> Sizedness for T {
        const FIXED_SIZE: bool = false;
    }
    impl<T /* : Sized */> Sizedness for T {
        const FIXED_SIZE: bool = true;
    }
    
    impl Unsized for str {}
    impl<T> Unsized for [T] {}
    impl Unsized for dyn ::core::any::Any + '_ {}

}

pub use private::is_sized;

fn main() {
    println!("Is i32 sized? {}", is_sized::<i32>());
    println!("Is str sized? {}", is_sized::<str>());
}

(Playground)

Output:

Is i32 sized? true
Is str sized? false

Alas, no :pensive:: for it to be exhaustive we'd have to know of all the possible dyn Trait combinations, which we can't. Only specialization can reach exhaustiveness; until then, users with their own traits would have to write impl Unsized for dyn TheirTrait + TheirSendSync + '_ {}

1 Like

Also, structs are allowed to directly contain one !Sized field, which in turn makes them unsized. You'll also need implementations like this:

pub struct MyWrapper<T:?Sized> {
    extra_info: usize,
    payload: T
}

impl<T:?Sized + Unsized> Unsized for MyWrapper<T> {}
2 Likes

Thanks for all this info. It helps me to understand Rust better (and get accustomed to playing with Traits). It's a lot of fun (for me at least).

That said, I figured out I don't need this particular function (which determines the sizedness) anymore. I still would like to work with generic implementations and negative implementations and negative coherence (other thread) though.

Some restrictions of Rust's type system are haunting me time to time. But on the other hand, even in stable Rust, the type system is already quite powerful. I think I'll have to decide on whether I want to use hacks and/or unstable features, or require the user of my library to make manual implementations where needed (on their types or newtypes).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.