Different placement of trait bounds

Hi, would appreciate some explanation on trait bounds related to the sample code below

Questions:
i can see at least 3 different places to put the Traits Bounds on the generic type T. My questions are:

  1. What/Why the differences - esp between traits_at_struct() & trait_at_impl()
  2. When should we use each different placement?
  3. Why doesn't the compiler complain at those places marked 'UNEXPECTED'?

Thanks

use std::fmt::Debug;

#[derive(Debug)]
struct A { val: u32 }

#[derive(Clone)]
struct B { val: String }

fn trait_at_struct() {
    struct C<T>  where T: Debug { val: T }

    // Expected - try passing A (Debug) to struct C<T> - Ok since A is Debug
    let my_c = C{val: A{val: 0}};

    // Expected - try passing B (Clone) to struct C<T> - does Not compile since B is Not Debug
    // let my_c = C{val: B{val: "B".to_string()}};
}

fn trait_at_impl() {
    struct C<T> { val: T }

    impl<T> C<T> where T: Debug {
        pub fn do_something(&self) {
            println!("{:?}", self.val);
        }
    }
    // Expected - try passing A (Debug) to struct C<T> - Ok since A is Debug
    let my_c = C{val: A{val: 0}};

    // UNEXPECTED:  ?? Why does this compile ??
    // try passing B (Clone) to struct C<T> - B is not Debug
    let my_c = C{val: B{val: "B".to_string()}};
    
    //my_c.do_something(); don't compile, 
    // why wait till i call do_something() before the compiler complains
}

fn trait_at_method() {
    struct C<T> { val: T }
    impl<T> C<T> {
        pub fn do_something(&self) where T: Debug {
            println!("val={:?}", self.val);
        }
    }

    // Expected - try passing A (Debug) to struct C<T> - Ok since A is Debug
    let my_c_1 = C{val: A{val: 0}};

    // UNEXPECTED: ?? Why does this compile ??
    // try passing B (Clone) to struct C<T> -
    //   ?? why doesn't the compiler say that the method do_something() requires T to be Debug ??
    let my_c_2 = C{val: B{val: "B".to_string()}};

    // Expected -try calling do_something on both my_c - ok compiles, since A is Debug
    let my_r_1 = my_c_1.do_something();

    // Expected - does not compile since B is not Debug
    // let my_r_2 = my_c_2.do_something();
}

In all of these cases, the bound only affects things when you try to use the thing the bound is on.

// UNEXPECTED: ?? Why does this compile ?? try passing B (Clone) to struct C<T> - B is not Debug

You didn't put a bound on C, so there's no prohibition on constructing a C<B>. The only thing the bound on the impl block prevents is using any of the items defined in that implementation, not the struct.

try passing B (Clone) to struct C<T> - ?? why doesn't the compiler say that the method do_something() requires T to be Debug ??

Because what putting the bound on do_something means is that you can't call do_something unless the bound is satisfied. Nothing more, and nothing less. Trait bounds on methods and on impls are the same thing — putting it on the block just saves you from writing it for every method or other item. In general, types can have methods with many different constraints.

Have a look around the standard library and you can find lots of examples:

  • Result::unwrap can only be called when the error type implements Debug (so it can be printed), but the basic Result enum, and most of its methods, can be used for any type.

  • Arc's purpose is to be a thread-safe (Sync) container, but it is perfectly happy to contain non-Sync types — it just doesn't itself count as Sync when you do that. This makes the library more flexible, because generic code doesn't have to restrict itself to only allowing thread-safe types if it wants to ever support being used in a threaded way. (This is about bounds on trait implementations rather than inherent implementations, though.)

It seems strange to me that in the case of trait_at_impl(), i am placing a trait-bound on the entire struct impl (hence on the struct?) and yet i can create a struct that violates the bounds?

I see the same argument on trait_at_method(), when the bound on T in the method definition is the same T defined at the impl level (hence the struct?) again?

I know how Result::unwrap requires Debug ... and i encountered this error ... had to decipher compiler error to discover this.

impls never constrain the struct itself. A type can have any number of impls, each with its own bounds. A common pattern is to have multiple impls like:

struct SomeData<T> { ... }

impl<T> SomeData<T> {
    // methods that work for any T
}

impl<T: Clone> SomeData<T> {
    // methods that work only when T can be cloned
}

And this works exactly the same for bounds on individual methods. Only the item with the bound is constrained, not the type.

Thanks for the explanation

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.