Why don’t we consider associated functions (no “self” argument) with dispatchable (first) argument object safe, too?

This idea seems like such a no-brainer, almost: The requirement for the dispatched argument of trait functions to be methods, i.e. taking a self argument, seems to have no technical motivation. Maybe it just grew this way historically? I’m having a surprisingly hard time finding prior discussion on this, please point out (i.e. link) anything that I’ve missed.

What this is about

Currently, a trait like

trait Frobnicable {
    fn frobnicate(self: &Self, arg: Information) -> Data;
}

is object safe, but a trait like

trait Frobnicable {
    fn frobnicate(not_self: &Self, arg: Information) -> Data;
}

isn’t.

But we could easily make this second version of Frobnicable considered object safe, too, as long as the type of the first argument still fulfills the same conditions as usual. Maybe it could even be so that any argument, not just the first, (but instead then, the only one that mentions Self type) could be dispatched on.

Why care?

The status quo of needing to have the dispatched argument be the first, and a self argument, isn’t technically limiting any functionality, so it’s not particularly limiting. Also, since the restrictions for the dispatched arguments are such that it’s a valid self-type, so it can be a method, you most often want a method anyways. Nonetheless… sometimes, you don’t want your API in a trait to be a method (or at least prefer not to have it be one). Maybe you don’t want to come up with a name that isn’t too likely to conflict with other methods of implementing types or other traits. Sometimes, you don’t really intend for the method to be (often) called by users at all.

Here’s the example where I’ve wanted this… feel free to point out others you can think of, too 😉

E.g. it can be useful to create some additional API in your trait just to support object safety. For example something like

trait VisitStrings: {
    fn for_each_str(&self, f: impl FnMut(&str));
}

can be made object safe by using a dyn FnMut callback… but you might not want the dynamic dispatch when you don’t need it. You can do this nicely with something like

trait VisitStrings: DynVisitStrings {
    fn for_each_str(&self, f: impl FnMut(&str))
    where
        Self: Sized;
}

trait DynVisitStrings {
    // currently *has* to be a method
    fn dyn_for_each_str(&self, f: &mut dyn FnMut(&str));
}
impl<T: VisitStrings> DynVisitStrings for T {
    fn dyn_for_each_str(&self, f: &mut dyn FnMut(&str)) {
        (self as &T).for_each_str(f)
    }
}

impl VisitStrings for Box<dyn VisitStrings + '_> {
    fn for_each_str(&self, mut f: impl FnMut(&str)) {
        <dyn VisitStrings>::dyn_for_each_str(self, &mut f)
    }
}

impl VisitStrings for &(dyn VisitStrings + '_) {
    fn for_each_str(&self, mut f: impl FnMut(&str)) {
        <dyn VisitStrings>::dyn_for_each_str(self, &mut f)
    }
}

and users can work with &dyn VisitStrings or Box<dyn VisitStrings> (or other pointer types, if such impls are added) still via the normal for_each_str method; dyn_for_each is more of an implementation detail, and perhaps useful occasionally for other smart pointers, but mostly it’s a method you don’t want to encourage being used, and making it not a method could help with making that clear.


Also now that I’m thinking about this… sometimes you’d want to apply above trick to achieve object safety on a trait where the original generic function (i.e. the one in the role of for_each_str above) wasn’t a method in the first place, in which case the restriction that you have to make the dyn_… a method seems particularly unnecessary

Semver concerns

Making more traits object safe comes with a meta semver concern. It’s not a breaking change itself, but it changes what changes are breaking. Such changes are (as far as I’m aware) often accepted[1], but for object safety here it could be affecting too many users (that’s a guess; I have not measured this in any way).

The problem here is two-fold. On one hand, crates could offer traits not intended to be object safe, which become object safe with this change. Their intention might have been future compatibility, with the desire to potentially add (default implemented) non-object-safe methods in the future. We would break this future compatibility. On the other hand, existing crates might have a trait which until now is not object safe in both versions, but this change might make it object safe on one and not object safe in the other version. I.e. the crate now suddenly has breaking changes between semver-minor versions.

Addressing these concerns is possible. For example object safety can be made opt-in

#[object_safe] // <- actual syntax might turn out different
trait Foo {}

and old editions could infer object safety as they do now, new editions never make a trait object safe implicitly, and the #[object_safe] opt-in is allowed in all cases allowed by the new extended set of supported method signatures in object safe traits.

(Discussion of a feature of explicit object safety annotations (in too much detail) should be mostly off-topic here. Regarding the semver concerns, I’m only interested in discussions about whether the semver concerns should be considered important in the first place, and possibly short descriptions of solutions, and/or links to other [maybe existing] discussions.)


  1. especially if running into this meta-breakage seems unlikely ↩︎

4 Likes

I feel like this has marginal utility, especially in light of the semver hazard this introduces.

I also don't quite get your example? Why would it be more obvious to users that they are not supposed to use it if it isn't a method? Wouldn't doc(hidden) be better of they really aren't supposed to use it? Or just a comment in the docs if it is discouraged.

5 Likes

Right… I do acknowledge that there’s probably better examples. I haven’t come up with them yet :wink:

I suppose this same kind of issue should also apply to async fn in traits (except, the utility of that is somewhat larger), where there are plans to develop ways to make them supported for object safe traits, as far as I’m aware. If that means that existing non-object safe traits become object safe, then we’d have the same meta semver issue.


I think there are other benefits of explicitly marking object safe traits, too. Implicits always come with the “semver hazard” of it not being obvious when you accidentally changed a thing that’s implicitly determined. I would be surprised if there aren’t many existing occurrences where people have accidentally made their object safe traits no longer object safe, without realizing that was a breaking change, and similarly I’m sure there’s many code bases that include explicit tests to ensure object safety of their traits isn’t accidentally removed.

Maybe a good question to answer is what the utility of a function that takes a dispatchable parameter that isn't &self (or &mut self,...) is?

I'm not sure I see the use case where you:

  • Need a parameter to not be a receiver
  • Yet be of a type that might as well be a receiver.
  • And also be object safe

In fact I fail to see why you would even need the first two of those (without the third one).

It would be required for a language that had multiple dispatch of course. But since rust doesn't have multiple dispatch (which I admittedly think is a neat feature, though I doubt it would be a good fit for Rust), I don't see the need for this.

A common example of making a receiver-type argument deliberately not a method is the API on smart pointers like Rc that don’t want to introduce conflict with methods of the Deref target. There’s a lot of this: Self and this: &Self arguments there.

That case does not involve object safety (and the functions aren’t even trait functions).

I’m not having any sources on this motivation being real[1] (i.e. discussions where someone would have explicitly brought up such resoning), but from what I can tell in the context of past extensions of what types could be dispatched on, the restriction to methods did effectively play a role in ensuring the same meta semver problems don’t occur. At the point when Rc and Arc were made to support dispatching in trait object, these types were made to be supported as self types in the first place, simultaneously. Thus, non-object-safe methods involving Arc<Self> arguments for example would remain non-object-safe, and only those that use the newly supported support for using such argument as a self: Arc<Self> argument for a method would be object safe, making the change thus opt-in in that regard.

For Pinned pointers to be supported, this support coincided with the stabilization of the Pin wrapper type itself, so there would have been no breakage there either way.


  1. not just an accidentally good design ↩︎

Note that you are proposing to relax not one, but two object-safety rules. Normally object-safe methods cannot contain Self type anywhere in parameters or return type. At this point your proposed rule relaxation feels like it adds more confusion while solving an extremely minor technical problem. Why is &Self as the first parameter allowed, but not as the second one? Why can't I use &Self as several parameter types? Or perhaps I can? But I can't guarantee during dynamic dispatch that the two erased types were really the same. Why is &Self allowed, but something like Rc<Self> is not? Or should it be allowed? What about more complex types?

At first I thought you just wanted to allow static methods on trait objects. After all, dyn Trait always exists behind some pointer (&dyn Trait, Box<dyn Trait> etc), and thus the exact erased type was known at some point. Thus we could add its static trait method fn foo() to the vtable, as a method without any receiver. We can't call it via the normal syntax Type::foo(), but we could conceivably still invent some syntax which would stand in place of Type in the static method call.

The biggest downside would be that dyn Trait no longer implements Trait, since obviously we can't manually write the relevant function implementation. On the other hand, static methods without Self type parameter are an actually common case, so perhaps it could be worth the trouble.

4 Likes

I agree that avoiding greater confusion is a valid concern / goal to keep in mind.

In my view, the status quo is that object-safety rules do mainly serve the purpose of reflecting the technical limitations in order for the dyn Trait type to be able to implement the Trait. This indeed motivates most rules perfectly, for example supertraits cannot contain Sized and all must be object safe, so that dyn Trait can implement all of them, too. In order for the compiler to write the method implementation, if the method doesn’t write where Self: Sized, the parameter that is being dispatched on must be of a type where the compiler knows how to extract the vtable and the original non-fat-pointer equivalent. Notably the fact that it’s a method isn’t technically necessary; and in my view, whether a function is a method or not,(and whether the Self-involving type is the first parameter) is something that should only have the purpose to determine the syntax with which the function can be called.

I’m mentioning the idea of allowing any parameter instead of the first to be used for dispatch deliberately as a separate thing to consider, because it is more complex to teach. Removing the requirement that it must be a method, and generalizing “method receiver type” to “type of first argument”[1] is a very straightforward extension. The rule still poses a certain set of dispatchable types supported for first parameter, and requires the other parameters not to involve Self at all. Further generalization to “any argument” requires more complex wording, identifying that “exactly one” argument involves the Self type, and that argument must be from a set of dispatchable types.


Also, my main interest with this thread isn’t just to propose such a feature [you see, I myself have trouble coming up with examples where this is particularly useful] but to explore why the restriction to “only methods” came about initially (i.e. if perhaps there are documented good reasons for its existence beyond “it’s always been this way and no-one considered extending the rule”), and to explore if the semver considerations I’ve described are – or should be – considered significant concerns against extensions to object safety rules.

Especially the latter relates to other, possibly much more valuable, extensions; for example the case of async fn I’ve already mentioned. One thing I’m thinking about recently is certain Self-involving return types. (Something like Box<Self> as a return type should be easy to support for object safe traits[2].)


  1. hoping that “first argument” for a method is correctly understood as the method receiver, not the first argument after that ↩︎

  2. going into details here can go off-topic, too, but I do not mean such a return type as something to dispatch on, so no fn() -> Box<Self>, but this is about such a return type in addition to a dispatched receiver type, like fn(&Self) -> Box<Self>, which could then in turn relieve some unsafety from crates like dyn-clone when such signatures become supported natively ↩︎

6 Likes

I think the technical reasons are obvious: dyn Trait is supposed to implement Trait, and the object-safe methods should be just ordinary methods of the trait implementation. This immediately excludes any static methods, regardless of parameter types. Your proposal is to change the usual static method call into a weird vtable lookup based on the first parameter type, which has no precedent in the language.

By the way, I think it can lead to an ambiguity. What if I actually can implement Frobnicable for dyn Frobnicable ? After all, the signature allows it in principle.

1 Like

As a side note, your motivating example could also be solved if we had private trait methods (as in methods that can only be called by other methods in the trait). That wouldn't need any changes to object safety rules.

How would this code compile?

pub struct Foo(u32);

pub trait MyAdd {
    fn my_add(&mut self, b: &Self);
}

impl MyAdd for Foo {
    fn my_add(&mut self, b: &Self) {
        self.0 += b.0;
    }
}

pub fn use_my_add(a: &mut dyn MyAdd, b: &dyn MyAdd) {
    a.my_add(b);
}

Making the trait object safe if only one of arguments is Self would be too surprising in my opinion.

That's really how it works today though, fn foo(&mut self) is just syntax sugar for fn foo(self: &mut Self), to be object safe there is just the pair of requirements: there is only one Self parameter and that parameter is named using the self keyword that gives method syntax; that second requirement isn't strictly necessary for any part of generating or using the vtable.

1 Like

Yes, but self is "special" despite it technically being a simple sugar. It would be more confusing to be object-safe for fn foo(a: &Self) and not be object safe for fn foo(a: &Self, b: &Self) at the same time. It's easier to teach and remember "you can not use Self as an argument or return value in object-safe traits outside of self". It's one rule, not two.

1 Like

Not quite, because there are other object safety rules as well. The different rule would be that an associated function is object safe if (all of the other rules and) the first argument is a method receiver, independent of whether it's bound to the pattern self or not.

Not quite, because you can call trait methods using UFCS (Trait::method(receiver)). <dyn Trait as Trait>::method is essentially just a function like any other, which does the vtable lookup to call the concrete function.


I'm personally a fan in theory, but it's enough of a departure that I think it'd need to be attached to an opt-in such as e.g. a dyn trait Name item declaration syntax which requires all trait items to be object-safe.

As an interesting aside, by-value receivers don't actually have an implied where Self: Sized bound (to remove them from dyn Trait needing to have an implementation) like I had originally thought, they're just considered object safe. (This only matters on stable in an edge case.)

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.