Why when a trait has an associated function cannot be _made into an object_?

Hi,

I am not sure to have really understood why this code is not compiling when I enable the commented part:

struct Foo {
    val: i32,
}

trait MyFrom<T> // // : Sized (it is kind of irrelevant to this issue)
{
    //fn my_from(other: T) -> Self;
    fn fake(&self);
}

impl MyFrom<i32> for Foo {
    // fn my_from(other: i32) -> Self {
    //     Foo{val: other}
    // }
    
    fn fake(&self){}
}

fn do_stuff(x: &dyn MyFrom<i32>){}

fn main() {
}

Why when a trait has an associated function cannot be made into an object?

I am asking do_stuff to accept a reference to a struct that is implementing MyFrom<i32> and even if MyFrom trait has an associated function, I should have the pointer to its implementation in the linked vtable, or at least this is what I am expecting (and probably it's wrong :blush:)

Thank you.

Without a self argument, you don't have an object to get the vtable itself from. Thus whatever pointers might be in the vtable, they are useless because you can't retrieve them.

2 Likes

Fair enough, but I still have some doubts, sorry :smiling_face:

I was expecting that this kind of error was raised if I was calling MyFrom::my_from<i32>, but in my case I am only declaring that do_stuff accepts a reference to an instance of a struct implementing MyFrom<i32>. So why bother about that?

So I cannot see why Rust is complaining about this issue as soon as I am declaring somewhere an argument or variable of type ...dyn MyFrom<...>

I am just asking to improve my Rust knowledge, of course it is not any kind of rant! :grinning_face_with_smiling_eyes:

The key thing to realize about object safety is that the compiler needs to automatically provide implementations of all the trait items¹ for dyn MyFrom<…>. There are two problems with implementing dyn MyFrom::my_from:

  1. It’s impossible to return an unboxed dyn … type from a method
  2. There is no way for the compiler to choose which concrete implementation to call (from all the explicit MyFrom implementations for various types)

¹ Except ones that are explicitly gated to Self: Sized


You don’t need MyFrom to be object-safe for this; you can use generics insread:

fn do_stuff<T: MyFrom<i32>>(x: &T){}
2 Likes

It would be bad if changing the implementation of the function (which isn't any of the interface's business) would affect whether its interface compiles.

1 Like

Your questions are, actually, surprisingly deep and highlight the key difference between C++ and Rust.

Basically they are described by the names: C++ have templates while Rust have generics.

And C++ templates are almost textual, they do almost all checks after instantiation and if some functions can not be instantiated then it's perfectly Ok (you just can't use these particular functions).

Rust follows “you may enter after security check” approach. All the requirements for the ability to use some trait would be verified when you would declare said trait. If Rust can not guarantee that these functions would be, actually, usable — trait is not accepted.

That, basically, means that no errors can be ever detected when you instantiate trait (except for the “internal compiler error”, of course), all the checking happens upfront.

It's all about radeoffs.

C++ approach makes it kinda-sorta-easier to declare template while Rust approach makes it easier to use it.

Does it mean that C++ approach is better? Nope, this just means that C++ guys found out that they couldn't switch to Rust way of doing things because of backward compatibility.

They tried, long ago but have found out that without breaking compatibility with almost the whole corpus of the existing code they may either implement such checks on the opt-in basis (this eventually happened) or break compatibility (which they couldn't afford).

Rust is a new language thus it just went and adopted C++ dream. Since it's a new language and backward compatibility wasn't a problem.

2 Likes

Yeah! You got me! :laughing: I am still too connected to C++ and also C#, that BTW it is more closed to Rust... I am little old and I spent 35 years of my life with C/C++ (and assembly at the very beginning!).
I am trying to approach to Rust with a different mindset paradigm, but sometime it is hard :blush:
I also trying to (very slowly) get into Haskell that I think it could help me to appreciate better some Rust features and I think it is very fascinating!

Anyway I think Rust is a way better language than C++, more robust, more consistent, more modern... A language built on lessons learned from other existing languages (and that's a great thing!)

Rust generics are rather like C# generics, in this way, just more powerful.

For example, you can't do this in C#

public void Foo<T>(T sequence) {
    foreach (var x in sequence) {
        Console.WriteLine(x);
    }
}

even if you only pass in sequences that happen to support foreaching.

You need to add where T: IEnumerable<int>, just like in Rust you'd need to have where T: Iterator<Item = i32>.

(And yes, in C# you'd often not use generics for this, but would use virtual function calls by just taking IEnumerable<i32> sequence as the parameter, but you can do that in Rust too by taking sequence: &mut dyn Iterator<Item = i32>. And in high-performance C# code you might want to do the generic form anyway, because monomorphizing it can sometimes help the JiT produce better code, especially if you use a struct as the enumerator.)

2 Likes

I don't see it mentioned explicitly by anyone else (2e71828 gets at it indirectly with their footnote) but you can include non-trait object safe things in a trait by gating them individually behind a Self: Sized bound rather than doing it to the trait as a whole, i.e.:

trait MyFrom<T>
{
    fn my_from(other: T) -> Self where Self: Sized;
    fn fake(&self);
}

impl MyFrom<i32> for Foo {
    fn my_from(other: i32) -> Self where Self: Sized {
        Foo{val: other}
    }
    
    fn fake(&self){}
}

(playground)

Of course, if your intent was being able to use my_from on a trait object, then this is not particularly helpful. :sweat_smile:

5 Likes

You should laso think of what the syntax to refer to my_from would be :sweat_smile:

It is not a method, so if you have x: &dyn MyFrom you can't do x.my_from(42), it would have to be <_ as MyFrom<i32>>::my_from(42), where the _ is either infered (which rustc can't just from this in a real case) or explicit (which would have to be something like <typeof(x) as MyFrom<i32>>::my_from(42).

For assoc functions, I usually would turn it into a method even though it doesn't technically needs it, so that I can call it with x.my_from(42). The same can be done for asoc consts, but not for assoc types.

1 Like

In the case of a trait object, nothing. It's not (and would never be) possible to call such a method "on a trait object", since there isn't any object to dispatch on. And with the hypothetical <typeof(x) as Trait> syntax, you would still need an object, at which point there'd be no difference from (or advantage over) just writing an object-safe method taking &self.

2 Likes