Use case for implementing a trait for a generic type

Hello

I am quite new to Rust and trying to understand the following snippet:

pub trait Bounded {
    fn aabb(&self) -> Aabb;
}

impl<T: Bounded> Bounded for &T {
    fn aabb(&self) -> Aabb {
        T::aabb(self)
    }
}

impl<T: Bounded> Bounded for &mut T {
    fn aabb(&self) -> Aabb {
        T::aabb(self)
    }
}

Is the above snippet is an example of the implementation of trait for a generic type or implementation of a generic trait for a generic type? To understand the difference between the two ideas, some reference to use case would be helpful.

Thanks

No. You are implementing a trait on a generic type that already implements it. That's what your bounds are saying.

I think traits are generic, except for cases involved in specialization. I forget generic is about generic parameter. See replies below from others.

The term for both impls is called blanket implementations:

A user-friendly definition of blanket implementations from the Book:

Implementations of a trait on any type that satisfies the trait bounds are called blanket implementations and are extensively used in the Rust standard library.
src: Traits: Defining Shared Behavior - The Rust Programming Language

A formal definition from the Reference:

Blanket implementation

Any implementation where a type appears uncovered. impl<T> Foo for T, impl<T> Bar<T> for T, impl<T> Bar<Vec<T>> for T, and impl<T> Bar<T> for Vec<T> are considered blanket impls. However, impl<T> Bar<Vec<T>> for Vec<T> is not a blanket impl, as all instances of T which appear in this impl are covered by Vec.

In this case, the &T and &mut T are called fundamental type constructors which are uncovered by its definition:

Fundamental type constructors

A fundamental type constructor is a type where implementing a blanket implementation over it is a breaking change. &, &mut, Box, and Pin are fundamental.

Any time a type T is considered local, &T, &mut T, Box<T>, and Pin<T> are also considered local. Fundamental type constructors cannot cover other types. Any time the term "covered type" is used, the T in &T, &mut T, Box<T>, and Pin<T> is not considered covered.

It can't be a generic trait; the trait Bounded has no generic parameters (neither type, lifetime, nor const parameters).

1 Like

Ignoring terminology for a moment, let's take a look at what the code does.

The first stanza declares the trait.

The second stanza implements the trait for any &T, provided T implements the trait. The implementation of aabb for &T takes a &&T as self and then calls T's aabb with a &T (one less reference nesting). It's not entirely obvious because they are relying on autoderef, but you can tell from the T::.

The third stanza is similar to the second.


If you implement the trait for YourType, the blanket implementations mean it will be implemented for &YourType, &mut YourType, &&mut &&&YourType, etc. Those just recursively call implementations with one less layer of nesting as discussed, until they eventually call your implementation.

If you had a

fn foo<T: Bounded>(t: T) { ... }

you could pass it a YourType or &YourType, etc.


I'd say the important part is to understand what types the implementations apply to.

3 Likes

The trait Bounded isn't generic as it has no type parameter. The types &T and &mut T are generic because they have a type parameter T.

Thus impl<T: Bounded> Bounded for &T and impl<T: Bounded> Bounded for &mut T are implementations of the trait Bounded for a generic type (&T and &mut T in this example).

I would say they are implementations "of a trait for a generic type".

It's unusual to implement a type, which is implemented for T, also for &T and &mut. I'm curious where this example comes from and what's the context.

1 Like

It's not at all unusual, it's quite common (and idiomatic) for traits that mostly perform read-only operations. Std itself does this for Display/Debug, for example. Serde's Serialize is transitive, too. Implementing transitively for mutable references only is common, too – std's Read and Write work like this, for example.

3 Likes

Oh, I wasn't aware of that. When exactly does it make sense to provide such implementations? I would assume when you sometimes pass an owned value and sometimes a reference to that value, and you want the trait to be fulfilled in both cases? So it's some sort of ergonomical trick?


Just saw @quinedot's response:

Still wondering when it makes sense to provide that functionality (and what are the downsides, if any?).

It makes sense when the trait only uses &self, or when it uses &mut self but you have interior mutability (like File, via your OS). The latter is more than ergonomic as maybe you can't provide a &mut File.

It's ergonomic for arguments, but more than ergonomic for owning containers. Consider what implementation makes Vec<T>: Debug, say.

1 Like

I remember the File case from when I started getting into Rust. However, it's different than the generic trait impls above: The impl Write for &File is specific to a particular type (File) and not generic, i.e. there is no impl<T: Write> Write for &T:

use std::io::Write;

fn write<T: Write>(_arg: T) {}

fn main() {
    let mut s: Vec<u8> = Vec::new();
    write(&mut s); // works
    //write(&s); // fails
    write(s); // works
}

(Playground)

So I guess it makes sense to impl<T: Trait> Trait for &T if all methods take a &self as receiver, and impl<T: Trait> for &mut T if all methods take a &self or &mut self (but never self) as receiver.

Okay, that makes sense. Thanks!

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.