Add additional field for a certain const generic struct

I have a const generic struct Foo:

struct Foo<const B1: bool, const B2: bool, const B3: bool> {...}

And I want to add additional fields for certain types:

struct Foo<const B1: bool, const B2: bool, const B3: bool> {
    pub f1: Vec<Foo<false, B2, B3>>, // only exitst in Foo<true, B2, B3>
    pub f2: String, // only exitst in Foo<false, B2, B3>
    pub id: usize, // exist in all types (Foo<B1, B2, B3>)
}

Can this be done? Currently, I have to warp f1, f2 with option (or union if I care the memory usage), which is not very elegant, and it's dangerous if others use f1 and f2 wrong.

P.S. In the example, the solution maybe this:

struct Foo<const B2:i32, const B3: i32> {
  pub id: usize
}
struct<const B2:i32, const B3: i32> TrueFoo(Foo<B2, B3>);
struct<const B2:i32, const B3: i32> FalseFoo(Foo<B2, B3>);

But the real case is more complicated, and it's hard to explain why I want to do this.
So is there a solution which I can add fields by using the const generic feature only? Unstable features (or even unfinished RFC) is acceptable.

Can the field name be the same? If not, you could have two types and one is () or something.

2 Likes

That's a solution, I didn't think about using associated types in traits this before. The only problem is that if there are a lot of these special fields, I have to write tons of traits, basically a combination of all possible types. That not easier than just make all the structs of certain types without using const generic

I don't know that it's any easier, but you could have one trait with parameters that mirror the struct. So that's a trait per struct potentially. Perhaps a macro could help with the ergonomics, like emulate cfg attributes or something.

Please do explain your motives. Wanting to add fields to a struct is a strange requirement without context. It is also likely that there are better solutions to your high-level problem.

Maybe what you are trying to do is have a single field with a type that itself depends on the outer type parameters?

1 Like

Maybe what you are trying to do is have a single field with a type that itself depends on the outer type parameters?

No, I just want to some certain fields are visible for certain types (which is depending on const generic combination), to make the type more safety, in case someone uses the field in wrong struct type.

For example, if someone writes implements for the example above:

impl Foo<true, true, true> {}

Then field f2 has nothing to do with you, and it is dangerous to use f2 in this case.
If you ask why it is dangerous? Because this type is an FFI type, it will pass to C as a pure struct Foo:

struct Foo {
    int B1;
    int B2;
    int B3;
    *FalseFoo f1;
    *char f2;
    usize id;
}

If you pass fields that do not meet the spec, the driver will crash.

Well, this is a strange way to express "different types", then – it's just that the designer of the driver API didn't know better/was lazy.

I would strongly suggest rewriting this using distinct structs depending on the condition (which is easily achieved via associated types as per @quinedot's suggestion).

it's just that the designer of the driver API didn't know better/was lazy.

I think you just jumped to conclusions. That's pretty common in driver dev, you can check the Vulkan spec, it's barely seen any type-safety structs. In driver dev, type safety is only exist in specs, not in codes (for a lot of reasons, goods and bads). That why I'm wrapping them to rust to force type-safety in language level.

I totally did. Also, I don't agree that:

  • it is good because "it has always been like that", or that
  • one shouldn't try to wrap unsafe low-level interfaces into type-safe abstractions.
1 Like

If the important thing is preventing the use/visibility of a given field, would it be possible to make it private and only provide a method for access with the right generics?

struct Foo<const B1: bool, const B2: bool, const B3: bool> {
    f1: Vec<Foo<false, B2, B3>>,
    f2: String,
    pub id: usize, // exist in all types (Foo<B1, B2, B3>)
}

impl<const B2: bool, const B3: bool> Foo<true, B2, B3> {
    fn f1(&mut self) -> &mut Vec<Foo<false, B2, B3>> {
        &mut self.f1
    }
}

impl<const B2: bool, const B3: bool> Foo<false, B2, B3> {
    fn f2(&mut self) -> &mut String {
        &mut self.f2
    }
}

// and so on
2 Likes

That a good solution, I care memory space too much that didn't come up with this idea before, it would be better if there's a solution that solve the memory usage, but this one is good to go.

Meaning that you simply wouldn't want to waste the space on inaccessible field, or there's something else other then that?

how about:

#[repr(C)]
pub struct MyStructWithFields<const F1: bool, const F2: bool, const F3: bool> {
    pub f1: ConstOpt<T1, F1>,
    pub f2: ConstOpt<T2, F2>,
    pub f3: ConstOpt<T3, F3>,
}

#[repr(transparent)]
pub struct ConstOpt<T, const V: bool>(pub <T as ConstOptImpl<V>>::Type);

pub trait ConstOptImpl<const V: bool> {
    type Type;
}

impl<T> ConstOptImpl<false> for T {
    type Type = ();
}

impl<T> ConstOptImpl<true> for T {
    type Type = T;
}
4 Likes

Yes, if there's no

 Foo<true, B2, B3>

Everywhere, then the f1 field should be better not take any space, as if it doesn't exist like this:

FooB2B3 {
// f1: Vec<...> not exist, not visiable, no space taken.
f2: string,
id: usize,
}

It won't compile:

#![feature(generic_const_exprs)]

pub trait OptField<const V: bool> {
    type Type;
}

impl<T> OptField<false> for T {
    type Type = ();
}

impl<T> OptField<true> for T {
    type Type = T;
}

struct Foo<const B1: bool, const B2: bool, const B3: bool> {
    f1: <i32 as OptField<true>>::Type, // works
    f2: <i32 as OptField<B1>>::Type, // won't compile
    f3: <i32 as OptField<{B1 || B2}>>::Type, // won't compile either, this maybe a issue of generic_const_exprs feature
}

Don't know why, but we are so close

I got it to work, I just forgot the trait bounds:

pub type T1 = i32;
pub type T2 = f32;
pub type T3 = String;

#[repr(C)]
pub struct MyStructWithFields<const F1: bool, const F2: bool, const F3: bool>
where
    T1: ConstOptType<F1>,
    T2: ConstOptType<F2>,
    T3: ConstOptType<F3>,
{
    pub f1: <T1 as ConstOptType<F1>>::Type,
    pub f2: <T2 as ConstOptType<F2>>::Type,
    pub f3: <T3 as ConstOptType<F3>>::Type,
}

pub trait ConstOptType<const V: bool> {
    type Type;
}

impl<T> ConstOptType<false> for T {
    type Type = ();
}

impl<T> ConstOptType<true> for T {
    type Type = T;
}
1 Like

That's awesome, I write a test case:

pub trait OptField<const V: bool> {
    type Type: Default;
}

impl<T: Default> OptField<false> for T {
    type Type = ();
}

impl<T: Default> OptField<true> for T {
    type Type = T;
}

struct Foo<const B1: bool, const B2: bool> where i32: OptField<B1>, i32: OptField<B2> {
    f1: <i32 as OptField<B1>>::Type,
    f2: <i32 as OptField<B2>>::Type,
    // f3: <i32 as OptField<{B1 || B2}>>::Type, // won't compile either, this maybe a issue of generic_const_exprs feature
}

impl<const B1: bool, const B2: bool> Default for Foo<B1, B2> where i32: OptField<B1>, i32: OptField<B2> {
    fn default() -> Self {
        Self {
            f1: Default::default(),
            f2: Default::default(),
        }
    }
}

fn main() {
    let mut foo1 = Foo::<true, false>::default();
    foo1.f1 = 1;
    // foo1.f2 = 2;
    let mut foo2 = Foo::<false, true>::default();
    // foo2.f1 = 1;
    foo2.f2 = 2;
    let mut foo3 = Foo::<true, true>::default();
    foo3.f1 = 1;
    foo3.f1 = 2;
    let mut foo4 = Foo::<false, false>::default();
    // foo4.f1 = 1;
    // foo4.f1 = 2;
}

The final problem would be the {B1||B2} syntax, I'll ask the const gereneric team to see if the generic_const_exprs do not support this syntax on purpose or not implemented yet.

this works:

#![feature(generic_const_exprs)]

pub trait OptField<const V: bool> {
    type Type: Default;
}

impl<T: Default> OptField<false> for T {
    type Type = ();
}

impl<T: Default> OptField<true> for T {
    type Type = T;
}

struct Foo<const B1: bool, const B2: bool>
where
    i32: OptField<B1>,
    i32: OptField<B2>,
    i32: OptField<{ B1 | B2 }>,
{
    f1: <i32 as OptField<B1>>::Type,
    f2: <i32 as OptField<B2>>::Type,
    f3: <i32 as OptField<{ B1 | B2 }>>::Type,
}

impl<const B1: bool, const B2: bool> Default for Foo<B1, B2>
where
    i32: OptField<B1>,
    i32: OptField<B2>,
    i32: OptField<{ B1 | B2 }>,
{
    fn default() -> Self {
        Self {
            f1: Default::default(),
            f2: Default::default(),
            f3: Default::default(),
        }
    }
}
1 Like

That's perfect, I can even now use i32 instead of boolean, which is more like my real case:

#![feature(generic_const_exprs)]

pub trait OptField<const V: bool> {
    type Type: Default;
}

impl<T: Default> OptField<false> for T {
    type Type = ();
}

impl<T: Default> OptField<true> for T {
    type Type = T;
}

struct Foo<const B1: i32, const B2: i32> where i32: OptField<{ B1 > 0 }>, i32: OptField<{ B2 > 0 }>, i32: OptField<{ (B1 > 0) & (B2 > 0) }> {
    f1: <i32 as OptField<{ B1 > 0 }>>::Type,
    f2: <i32 as OptField<{ B2 > 0 }>>::Type,
    f3: <i32 as OptField<{ (B1 > 0) & (B2 > 0) }>>::Type,
}

impl<const B1: i32, const B2: i32> Default for Foo<B1, B2> where i32: OptField<{ B1 > 0 }>, i32: OptField<{ B2 > 0 }>, i32: OptField<{ (B1 > 0) & (B2 > 0) }> {
    fn default() -> Self {
        Self {
            f1: Default::default(),
            f2: Default::default(),
            f3: Default::default(),
        }
    }
}

fn main() {
    let mut foo1 = Foo::<1, 0>::default();
    foo1.f1 = 1;
    // foo1.f2 = 2;
    // foo1.f3 = 3;
    let mut foo2 = Foo::<0, 1>::default();
    // foo2.f1 = 1;
    foo2.f2 = 2;
    // foo2.f3 = 3;
    let mut foo3 = Foo::<1, 1>::default();
    foo3.f1 = 1;
    foo3.f2 = 2;
    foo3.f3 = 3;
    let mut foo4 = Foo::<0, 0>::default();
    // foo4.f1 = 1;
    // foo4.f2 = 2;
    // foo4.f3 = 3;
}