Different implementations of a struct depends on whether it has the given trait or not

I am a newbie to rust and now in trouble with generics and trait.
First, I define a trait like this.

pub trait MyPrint {
    fn print(&self);
}

Then I want to define another struct, to check whether a type has trait MyPrint, and implement the corresponding function. Pseduo code as below

struct Checker<T> {
    phantom: std::marker::PhantomData<T>,
}

impl<T> Checker<T> {
    fn check() where T: MyPrint {
        println!("Type T implements MyTrait");
    }

    fn check_not() where T: !MyPrint {
        println!("Type T does not implement MyTrait");
    }
}

However, I got the error (negative bounds are not supported). I' d appeciate it if anyone could give me some suggestions on it.

Negative bounds are indeed not stable. However, they also don't mean what you probably think they mean. For a negative bound to hold, a type has to explicitly opt out of implementing the trait:

impl !MyPrint for MyStruct {}

So T: !MyPrint doesn't mean "T doesn't implement MyPrint (currently)", it means "T has promised not to implement MyPrint (ever[1])".


What you're asking about isn't directly possible, and my phrasing above may have given you a clue as to why: If it was possible, it would become a major breaking change to implement MyPrint. Any code calling Checker::<T>::check_not() would suddenly fail to compile. Implementing new traits isn't supposed to be a breaking change.[2]

You can kinda-sorta approximate things by implementing one function for everybody ("maybe implements MyPrint") and another function for types that definitely implement MyPrint.


  1. it's a breaking change, so they could only reasonably do so in a new major release ↩︎

  2. to the extent possible; there are some niche exceptions ↩︎

3 Likes

You are totally right. Checker::<T>::check_not() would be dangerous for those Ts who implement MyPrint.

Now I have a new idea. Checker would implenment only one function has_or_not, maybe sth like

impl<T> Checker<T> {
/*  
    Only one function here.
    If T  implements MyTrait, `has_or_not` function would be defined as
    fn has_or_not() {
        println!("Type T implements MyTrait");
    }

    otherwise `has_or_not` function would be defined as
    fn has_or_not() {
        println!("Type T does not implement MyTrait");
    }
*/
}

Then Checker::<T>::has_or_not() might be safe for all T . Is it possible?

Kinda-sorta. You could have a trait with the method, implemented for any Checker<T>, and an inherent method only defined when T: MyPrint.

Both methods will exist when T: MyPrint; if there's a &self receiver (say), method resolution will usually result in the expected method being called outside of generic contexts. I still don't really recommend it as it's not idiomatic and can be surprising... and doesn't work in generic contexts.

On mobile but I can make an example of the pattern and it's shortcomings later if you want.

Specialization would fix many of the shortcomings but probably isn't coming anytime soon.

1 Like

This is a case of an XY Problem. Why do you want to do print different things for types depending on if they implement a specific trait? What's your real end goal? Traits aren't intended for that, and most likely there is a better, simpler solution, perhaps not involving traits or generics at all. You shouldn't be trying to inspect what traits a type implements at run-time, it should be determined at compile time (by constraining function parameters to be required to implement the trait).

I'd like to provide you some example code to point you in the right direction, but I don't know what the goal is here, especially since print() isn't used anywhere. Also, I'm pretty sure there's no reason for the Checker struct to exist (if its functionality is needed at all, it could probably just be two free-standing functions).

3 Likes

I should apologize for providing inappropriate examples so as to make others confused. The motive is as belows:

In cpp we could judge whether a template class has the certain functions by concept and requires. Here is an example I found from the Internet.

template <typename PointAttribute>
concept has_normal = requires(PointAttribute t)
{
   t.nx();
   t.ny();
   t.nz(); 
};

template <typename PointAttribute>
void func(const PointAttribute &p)
{
   if constexpr (has_normal<PointCloud::point_attribute>){
      // do sth
   }else{
      // otherwise 
   }
}

I just wonder how to implement the similar things in rust.

C++ templates are more powerful than similar tools in Rust (for better AND for worse). So certain manners of abstraction are just not going to be as viable in Rust as they are in C++. This generally just means you'll need to do it a different way in Rust, probably with less abstraction or simpler abstractions.

quinedot is answering your question at face value (which is reasonable), but I would encourage starting with the actual real-world problem you're facing, and solve it with as little abstraction as possible, then only add abstraction where it becomes obvious that it is needed (frequently it isn't), rather than trying to implement the same abstractions you're used to in C++. Rust isn't C++: you have to write it differently. You can't translate every concept 1-to-1 and that's fine.

A general solution to the your latest example would be an ECS (Entity Component System), which allows you to give arbitrary tags to different types, then perform different logic depending on if a tag is present or not. You would create a HasNormal tag and tag whatever types you want with it. ECS is common in complicated game development (it's much more flexible than implementing complex object hierarchies). Bevy is the standard ECS in Rust, so good to look into. This is a heavy-weight solution to the problem, but it's what's needed sometimes.

If you're not making a complicated game or similar, then most likely you should just scrap the complicated-abstractions approach instead, as I mentioned before. Again, try to move those runtime checks to compile-time. 99% of the time you shouldn't be trying to inspect types at runtime. You should just have functions that take types that implement a trait HasNormal as a parameter and you'll make your life so much simpler.

3 Likes

This feature is specialization, and Rust doesn't have it yet.

2 Likes

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.