[Unstable] Specialization of const generics using a generic const fn

I wanted to make a container type with a field which type is different depending on a type T.
I tried using const generics and a const fn to determine that type. Like this:

#![allow(incomplete_features)]
#![feature(const_generics)]

trait FooTrait { type Output; }

struct Foo<const BAR: bool>;
impl FooTrait for Foo<true> { type Output = u32; }
impl FooTrait for Foo<false> { type Output = u64; }

const fn bar<T>() -> bool {
    std::mem::size_of::<T>() <= 4
}

struct Container<T> {
    _field: <Foo<{bar::<T>()}> as FooTrait>::Output
}

fn main(){
    let _d = Container::<i32>{_field:3};
}

If I make the Container type non generic, it works just fine, but this code gives 2 errors:

error[E0277]: the trait bound `Foo<{bar::<T>()}>: FooTrait` is not satisfied
  --> src/main.rs:21:5
   |
21 |     _field: <Foo<{bar::<T>()}> as FooTrait>::Output
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FooTrait` is not implemented for `Foo<{bar::<T>()}>`
   |
   = help: the following implementations were found:
             <Foo<false> as FooTrait>
             <Foo<true> as FooTrait>

error[E0392]: parameter `T` is never used
  --> src/main.rs:20:18
   |
20 | struct Container<T> {
   |                  ^ unused parameter
   |
   = help: consider removing `T`, referring to it in a field, or using a marker such as `std::marker::PhantomData`

Why doesn't the compiler recognize that {bar::<T>()} is a bool, while it recognized {bar::<i32>()}?
And are there alternatives to make this work?

I'm guessing the constant evaluator isn't smart enough to know that Foo<true> and Foo<false> cover all the possible constants for Foo<const BAR: bool>. Kinda like how in the past matching on a u8 would need a catch-all branch (_ => ...) even if you covered every possible value with range patterns (e.g. match some_u8 { 0..128 => foo, 128..=255 => bar } ).

I got this working with the help of specialisation. What I did was create a generic impl for all const B: bool, then specialised the true variant.

#![allow(incomplete_features)]
#![feature(const_generics)]
#![feature(specialization)]

use std::marker::PhantomData;

trait FooTrait {
    type Output;
}

struct Foo<const BAR: bool>;
impl FooTrait for Foo<true> {
    type Output = u32;
}
impl<const B: bool> FooTrait for Foo<B> {
    default type Output = u64;
}

// some helpers to clean up the line-noise from generics
type Bar<T> = Foo<{ bar::<T>() }>;
type FooOutput<T> = <Bar<T> as FooTrait>::Output;

const fn bar<T>() -> bool {
    std::mem::size_of::<T>() <= 4
}

struct Container<T> {
    _field: FooOutput<T>,
    /// A marker so the compiler thinks `T` is actually used as part of the type.
    _phantom: PhantomData<T>,
}

fn main() {
    let _d = Container::<i32> {
        _field: 3,
        _phantom: PhantomData,
    };
}

(playground)

1 Like

Thanks! I didn't knew about the default keyword for specialization.

And as a side note: Why is the phantom data necessary here?
You use T in FooOutput<T>. Isn't that enough for the compiler to think T is part of the type?

That was my initial reaction too. I'm guessing it's because the type that FooOutput<T> expands to has nothing to do with T?

You may want to create an issue against the Rust repo asking why implementing FooTrait for Foo<true> and Foo<false> wasn't enough to satisfy Foo<const BAR: bool> : FooTrait. There's a good chance it just hasn't been implemented yet or they may be leaving it for future work, but seeing your use case and one way to work around it may be helpful if this is a bug.

1 Like

If you're already playing around with unstable features, you may find the tracking issue for specialisation interesting. It's got links to things like the official RFC, a to-do list, historical discussions, etc.

1 Like

Note: specialization doesn't really work with associated types,

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

1 Like

I remember reading that if you specialise an associated type you can't make any assumptions about it because downstream implementors could give it a different type. So this is intended behaviour (relevant section in the RFC).

That's why it's perfectly fine to use Container::<u8> and _field: 3. Letting T: u8 means we'll eventually use Foo<true>::Output which can't be overridden (we didn't use the default keyword to allow further specialisation), and so it's possible to reason about it.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.