Type traits like in C++ possible

Hallo,

is it possible with Rust to create a struct which contains one generic value but this is bound to specific types? In C++ I would use std::is_integral - cppreference.com and other stuff to create bounds that are checked at compile time and automated deduced.

The goal is to create function that could take the specified values and the use has not to specify the type.

For example:

pub struct PropertyData<T> 
where T: Boolean + Integer + ByteVector
{
    value: T,
}

//impl ...

Alternatives would be an Enum or create a separate function for each type but there is no automated deduction.

Thanks

Yes, a Rust struct can be generic, although it's more idiomatic to put the trait bounds on specific impl blocks rather than restricting the entire type unless that's needed for some technical reason. So you would write struct PropertyData<T> { value: T } and then specify where T: … in impl blocks only.

Some possibilities:

#![allow(dead_code)]

pub enum BoolIntBytesEnum {
    Bool(bool),
    Int(i32),
    Bytes(Vec<u8>),
}

pub struct PropertyDataWithEnum {
    value: BoolIntBytesEnum,
}

fn use_enum(arg: PropertyDataWithEnum) {
    use BoolIntBytesEnum::*;
    match arg.value {
        Bool(b) => println!("Got bool {b}"),
        Int(i) => println!("Got integer {i}"),
        Bytes(blob) => println!("Got blob of length {}", blob.len()),
    }
}

// Or alternatively:

pub trait BoolIntBytesTrait {
    fn bool(&self) -> Option<bool>;
    fn i32(&self) -> Option<i32>;
    fn bytes(&self) -> Option<&[u8]>;
}

impl BoolIntBytesTrait for bool {
    fn bool(&self) -> Option<bool> {
        Some(*self)
    }
    fn i32(&self) -> Option<i32> {
        None
    }
    fn bytes(&self) -> Option<&[u8]> {
        None
    }
}
impl BoolIntBytesTrait for i32 {
    fn bool(&self) -> Option<bool> {
        None
    }
    fn i32(&self) -> Option<i32> {
        Some(*self)
    }
    fn bytes(&self) -> Option<&[u8]> {
        None
    }
    
}
impl BoolIntBytesTrait for Vec<u8> {
    fn bool(&self) -> Option<bool> {
        None
    }
    fn i32(&self) -> Option<i32> {
        None
    }
    fn bytes(&self) -> Option<&[u8]> {
        Some(self)
    }
}

pub struct PropertyDataWithTrait<T> {
    value: T,
}

fn use_trait<T>(arg: PropertyDataWithTrait<T>)
where
    T: BoolIntBytesTrait,
{
    if let Some(b) = arg.value.bool() {
        println!("Got bool {b}");
    }
    if let Some(i) = arg.value.i32() {
        println!("Got integer {i}");
    }
    if let Some(blob) = arg.value.bytes() {
        println!("Got blob of length {}", blob.len());
    }
}

fn main() {
    let x = PropertyDataWithTrait {
        value: true,
    };
    use_trait(x);
}

(Playground)

Output:

Got bool true

Note that using the enum will erase the type argument entirely (and make it fully dynamic always), which might not be what you want(ed).

:+1: I learned that too, and followed that rule in my above example also.

After some thinking and with the help/hints of you I was able to solve my problem so far. At least the first step. :grinning: I use now following approach to begin with:

trait DataTypes {}

struct Data<T: DataTypes> {
    value: T, 
}

impl<T> Data<T> 
    where T: DataTypes
{
    pub fn new(value: T) -> Self {
        Self {
            value
        }
    }
}

impl DataTypes for bool {}
impl DataTypes for i8 {}
impl DataTypes for u8 {}
impl DataTypes for String {}
impl DataTypes for Vec<u8> {}

fn main() {
    let bool_data = Data::new(false);
    let i8_data = Data::new(4i8);
    let u8_data = Data::new(6u8);
    let string_data = Data::new("Hallo".to_owned());
    let vec_data = Data::new(vec![1, 2, 3, 4]);
}

Thanks for your help.

Note that this doesn't obey the best practice I described above. The trait bound on both the struct declaration and the current impl block is superfluous – you could and should leave it off, so:

trait DataTypes {}

struct Data<T> {
    value: T, 
}

impl<T> Data<T> {
    pub fn new(value: T) -> Self {
        Self {
            value
        }
    }
}

impl DataTypes for bool {}
impl DataTypes for i8 {}
impl DataTypes for u8 {}
impl DataTypes for String {}
impl DataTypes for Vec<u8> {}

fn main() {
    let bool_data = Data::new(false);
    let i8_data = Data::new(4i8);
    let u8_data = Data::new(6u8);
    let string_data = Data::new("Hallo".to_owned());
    let vec_data = Data::new(vec![1, 2, 3, 4]);
}

You should only apply the T: DataTypes trait bound when it's actually needed. Also, don't call it DataTypes; call it DataType in singular. Naturally, traits can be implemented for more than one type (that's basically the whole point of abstracting over types!), so it's superfluous and non-idiomatic to call them by plurals.

You are right with the names. This is only an example and will be renamed anyway.

Should not went the where-clause to Data::new()? Otherwise all types are accepted without implementing Datatypes.

impl<T> Data<T> 
{
    pub fn new(value: T) -> Self 
        where T: DataTypes
    {
        Self {
            value
        }
    }
}
-struct Data<T: DataTypes> {
+struct Data<T> {
     value: T, 
 }

This is what @H2CO3 meant. It's not necessary to add a bound on the Data struct. (I know it's unusual, but you usually omit the bound in the struct when possible.)

Yes. I understood. But if I used it this way I can add all types to it. Even one who aren't implementing DataTypes

See:

use std::collections::HashMap;

trait DataTypes {}
struct Data<T> {
    value: T, 
}

impl<T> Data<T> 
{
    pub fn new(value: T) -> Self 
        //where T: DataTypes
    {
        Self {
            value
        }
    }
}

impl DataTypes for bool {}
impl DataTypes for i8 {}
impl DataTypes for u8 {}
impl DataTypes for String {}
impl DataTypes for Vec<u8> {}


fn main() {
    let bool_data = Data::new(false);
    let i8_data = Data::new(4i8);
    let u8_data = Data::new(6u8);
    let string_data = Data::new("Hallo".to_owned());
    let vec_data = Data::new(vec![1, 2, 3, 4]);

    let hash_value: HashMap<u16, u16> = HashMap::new();
    let hash_data = Data::new(hash_value); //with where-clause it won't compile (wanted behavior)
}

Without the where-clause in Data::new I am able to use a type that doesn't implement DataTypes. With the where-clause it won't compile.

You can keep the where-clause in the impl but only remove : DataTypes from the struct definition.

But there is no harm in being able to construct such a value! It might even save the day in generic/macro-heavy code. Unless it causes memory unsafety, you should not restrict construction. Restrict only the operations that actually need to use the trait's methods or rely on the trait being implemented for memory safety.