Why use trait bounds in struct definitions with generic type parameters?

892 views Asked by At

I can define a struct type that uses a generic type parameter with a trait bound:

struct MyStruct<T: Clone> {
    field: T,
}

This prevents me me from instantiating MyStruct with a generic type which does not meet the trait bound:

// Note: does not implement Clone
struct UnitStruct;

fn main() {
    // ERROR: Unsatisfied trait bound: UnitStruct: Clone
    let s = MyStruct { field: UnitStruct };
}

But why would I want to define my struct this way? What are the use cases of imposing such limitations on the instantiation of MyStruct?

I noticed that even with the trait bound in the MyStruct definition, if I define an interface which uses MyStruct, I still have to repeat the trait bound:

// This works
fn func<T: Clone>(s: MyStruct<T>) -> T { s.field.clone() }

// This does not. Compiler demands a trait bound for `T`
fn func<T>(s: MyStruct<T>) -> T { s.field.clone() }
2

There are 2 answers

0
Locke On

Typically you want to avoid adding trait bounds to structs when possible. When you add trait bounds this way, you will have to write out the trait bounds again on every impl<T: A + B + C + Etc> for Foo<T>. This is more work for you and can even reduce readability if not all of those traits are actually necessary for the impl block you are writing. Traits like Clone that do not have associated types or static functions probably shouldn't be included in the type bounds unless you have a specific reason to do so.

The reason you can add trait bounds this way is because it can greatly improve readability and there are some cases where it is largely unavoidable such as with associated types or fields which must be Sized.

use std::sync::mspc::Receiver;

trait FooProcessor {
    type Input: Sized + Send;
    type Output: Sized;

    fn do_foo(&mut self, input: Self::Input) -> Self::Output;
}


struct FooHandler<P: FooProcessor> {
    processor: P,
    input_channel: Receiver<P::Input>,
    outputs: Vec<P::Output>,
}

We can avoid this by expanding to add more type parameters, but that gets messy quickly. This can greatly reduce code readability and it gets worse if you have multiple nested generic structs.

struct FooHandler<P, I, O> {
    processor: P,
    input_channel: Receiver<I>,
    outputs: Vec<O>,
}

Another very important reason is to avoid mistakes. If it doesn't make sense for a type to be in a generic field, then you can get the compiler to catch it early by enforcing it on the struct parameters. It is not a good feeling to have written your code around the idea of using a Foo<Bar> only to discover that Bar was never actually a valid option.

0
YthanZhang On

The trait bound limits what type can be used in generic functions or structs, but also enables the generic T to do things it otherwise cannot.

For example

/// This function doesn't compile, because T cannot be cloned
fn clone_and_return1<T>(t: &T) -> T {
    t.clone()
}

/// This compiles, because we limit T to types that implements Clone
fn clone_and_return2<T>(t: &T) -> T
where
    T: Clone,
{
    t.clone()
}

So add trait bounds when you need T to have certain behavior.

If you need MyStruct to be Clone, then everything it owns must also be Clone, so T: Clone would make sense.

However, if that's not required, it's best to not include such trait bound.