Specializing based on const values/fns?

I'm reading through the source for RawVec, and it seems odd that it's all following the same code path for ZSTs and types of other sizes. But for obvious reasons, it has no real alternative other than a bunch of repetitive if std::mem::size_of::<T>() != 0 { ... } else { ... } in every call (which it could do).

Specialization does bring many useful things to the table, and I could foresee this as one potential application, simplifying RawVec for that. There's also a few other cases where it might be useful, like Option<T> and Result<T, E> where std::mem::size_of::<T>() and std::mem::size_of::<E>() are both less than std::mem::size_of<isize>() - this lets you make that optimization without baking it directly into the compiler. And of course, one could imagine much more complicated cases than these.

2 Likes

Going even further, specialization of struct contents would be great. Like a specialization of std::thread::LocalKey for non-Drop types so that they don't contain two unused bools.

You could also do it with mutually exclusive impls based on the value of an associated constant.

Then what you could write it as:

trait Size:Sized{
    const SIZE:usize=std::mem::size_of::<Self>();
    const IS_ZST:bool=Self::SIZE==0;
}

impl<This> Size for This{}

impl<T, A: Alloc> RawVec<T, A> 
where
    T:Size<IS_ZST=true>
{
    fn allocate_in(capacity: usize, zeroed: bool, mut a: A) -> Self { ... }
}

impl<T, A: Alloc> RawVec<T, A> 
where
    T:Size<IS_ZST=false>
{
    fn allocate_in(capacity: usize, zeroed: bool, mut a: A) -> Self { ... }
}

I don't see how specialization would make this any less repetitive,you would have to write the 2 impls based on the size of T anyway.

Can you expand on what you mean by this? Enum discriminants are already as small as they need to be(stored separately from their payload if the discriminant can't be stored in the payload).

I would consider that a complication, not a simplification. It transforms a local problem (code with branches) into a global one (having to look up which specialized impl is used for the particular type I am currently dealing with), which makes reasoning harder. One can't just accidentally ignore an if or match expression inside the function body, but it's easy to slip over the existence of a specialized impl (which, God forbid, might even be in a different module/file/directory!).

1 Like

I agree with you on functions. But there are cases that control flow expressions are not enough, for example, what is the alternative to C++'s std::conditional<true, int, double>::type ? Is it possible to write type T = if const_val { i32 } else { f32 } ; in the future?

Constant Generics still have a lot of work before it's ready but enough is done that you can already do that.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=9b4899ea1be98055866632c9334a723c

2 Likes

I hope not. I tend to be wary of wanting to pull C++ idioms and their supporting elements one-to-one into Rust. Rust has many advanced features (esp. in the type system) that C++ doesn't sport and which ultimately obviate the need for several helpers, hacks, and workarounds required in C++.

I must admit, though, that std::conditional seems powerful. (I'd probably still make a sound of discomfort upon encountering something like it in a Rust codebase.) What's the use case you have in mind for it?

type_level_conditional! {
    type T = if { ::core::mem::size_of::<Foo>() == 0 } {
        i32
    } else {
        f32
    };
}

works in nightly.

See https://github.com/rust-lang/rust/issues/64494 for another take on this sort of thing

Unfortunately, that does not (yet) work in a generic context, as the compiler doesn't know that the impls for true and false cover all possibilities:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=a06e517cfedff4e90feebe5ca5b97f12

This has been discussed before:

As discussed in that thread, a possible workaround is using specialization (and in this example the never type, both also still unstable features). Adding a generic impl like the following works:

impl<True, False, const B: bool> Helper for ITE<True, False, {B}> {
    default type T = !;
}

playground

Because we have specialized impls for ITE<True, False, {true}> and ITE<True, False, {false}> the default impl is never actually used, and we can use this construct in generic contexts.

As for when you would want constructs like these; anytime you want to have a different layout depending on a const value. Toy example:

enum Encoding { UTF8, UTF16, UTF32 }

#[repr(transparent)]
struct CodeUnit<const E: Encoding> {
    raw : ... // u8, u16 or u32 depending on the Encoding
}