Impossible to use Any combined with any other trait?

I basically need dyn Any + Clone. Clone isn't object safe, which lead me to find the dyn_clone crate with the DynClone trait so then I tried dyn Any + DynClone. But as far as I can tell, for different reasons depending on how you try to use it, there is no way to use Any downcasting with any other trait! Here are my attempts with error messages:

#![allow(unused_imports)]
#![allow(unused_mut)]
#![allow(unused_variables)]
#![allow(dead_code)]

use std::any::{Any, TypeId};

trait Extra {}

trait SubAny: Any + Extra {}

struct Bar {}

// works fine, doesn't combine Any with other traits
fn downcast1(foo: &dyn Any) -> Option<&Bar> {
    foo.downcast_ref::<Bar>()
}

// Try using a Trait that combines Any and another trait
// error[E0599]: no method named `downcast_ref` found for reference
// `&(dyn SubAny + 'static)` in the current scope
// fn downcast2(foo: &dyn SubAny) -> Option<&Bar> {
//     foo.downcast_ref::<Bar>()
// }

// Try using a trait that extends Any without adding in another trait... even this doesn't work
// error[E0658]: cannot cast `dyn SubAny` to `dyn Any`, trait
// upcasting coercion is experimental
// fn downcast3(foo: &dyn SubAny) -> Option<&Bar> {
//     <dyn Any>::downcast_ref::<Bar>(foo)
// }

// Give up on the super trait idea and make peace with copy pasta everywhere... nope still doesn't work
// error[E0225]: only auto traits can be used as additional traits in
// a trait object
// fn downcast4(foo: &dyn Any + Extra) -> Option<&Bar> {
//     foo.downcast_ref::<Bar>()
// }

// maybe if we call it a different way? nope
// error[E0225]: only auto traits can be used as additional traits in
// a trait object
// fn downcast5(foo: &dyn Any + Extra) -> Option<&Bar> {
//     <dyn Any>::downcast_ref::<Bar>(foo)
// }

Context where I'm trying to use this: factory that works with arbitrary query/reponse types. I register handlers that work for a particular query/reponse type pair, users give the factory queries of a particular type with a particular expected response type, then the factory dispatches to the appropriate registered handler. I need Any for the type erasure, and DynClone because the requests are async and I want the request to get its own copy of the query so that QueryType: Send is sufficient without needing Sync.

2 Likes

What you essentially want is supertrait coercions, which the language doesn't support yet. (There's no deep technical or theoretical reason behind that – it was simply not yet decided which one of the myriad possible implementation strategies would be the best.)

A possible workaround for now is to require the subtrait to implement an explicit upcasting method to each relevant supertrait, eg.:

trait Upcastable: Any + DynClone {
    fn as_any(&self) -> &dyn Any;
    fn as_clone(&self) -> &dyn DynClone;
}
5 Likes

Thanks! Easier to workaround than I feared.

@H2CO3 Actually the obvious method implementation did not work:

trait QueryTrait: Any + Send + DynClone {
    /// Workaround for Rust lacking trait upcasting support.
    fn as_dyn_clone(&self) -> &dyn DynClone {
        return &self;
    }

    /// Workaround for Rust lacking trait upcasting support.
    fn as_any(&self) -> &dyn Any {
        return &self;
    }
}

I tried returning self, &self, &*self, and self as &dyn DynClone; but they all give different errors. How do I actually implement the methods? It seems like all the information the compiler needs is there. Usually returning a reference to self or something in it when taking self is fine, I'm not sure why this is different.

The problem is that the Self type might be dynamically sized in which case the &self reference would already be a fat pointer. The solution is to preform the cast in a blanket implementation that requires the Self type to be Sized.

trait QueryTrait: Any + Send + DynClone {
    /// Workaround for Rust lacking trait upcasting support.
    fn as_dyn_clone(&self) -> &dyn DynClone;

    /// Workaround for Rust lacking trait upcasting support.
    fn as_any(&self) -> &dyn Any;
}
impl<T> QueryTrait for T where T: Any + Send + DynClone + Sized {
    fn as_dyn_clone(&self) -> &dyn DynClone {
        self
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

If you have other methods in the trait then you can split the cast methods into their own trait:

trait QueryTrait: DynAnyClone {
    fn other_mehod(&self);
}

trait DynAnyClone: Any + Send + DynClone {
    /// Workaround for Rust lacking trait upcasting support.
    fn as_dyn_clone(&self) -> &dyn DynClone;

    /// Workaround for Rust lacking trait upcasting support.
    fn as_any(&self) -> &dyn Any;
}
impl<T> DynAnyClone for T where T: Any + Send + DynClone + Sized {
    fn as_dyn_clone(&self) -> &dyn DynClone {
        self
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

Why is it a problem if it's already a fat pointer?

Thinking it through: If it were say a fat pointer to a slice, which should be an address and size, I'd expect if it becomes a dyn reference for it to be a fat pointer that is a vtable address, a size, and an address. I was tempted to say the size could go in the vtable but then you couldn't reuse the vtable and you would have to allocate one. So there's no stabilized 3-element fat pointer to deal with this situation?

Right, the only wide pointers are currently 2 usize large, with the second usize being either a vtable pointer or the number of slice elements.

I think we do have a feature-gated implementation on nightly already. Such features are not part of “the language” of course, but it might be interesting for people to play around with.

1 Like

There is a reason why I didn't provide a default implementation :wink: If you could have those, then this would be a "solution", not a "workaround", since it would imply no boilerplate at all. For now though, you have to implement the upcastng methods manually on every implementing type. This is for the same reason why simple supertrait coercion doesn't work out of the box (you'd have to fatten the fat pointer further when the method is invoked on a trait object).

1 Like

Nice, I didn't know that.

It sounds though like you shouldn't need to do that as long as you establish the type is sized? trait QueryTrait: Sized should be enough to let default implementations work? I'd try this now but I'm on mobile :sweat_smile:

But then you can't make it into a trait object. Similarly, if you restrict the upcasting methods only with where Self: Sized, then you can't call them on a trait object.

FYI, here is the tracking issue for dyn upcasting. The RFC for trait object upcasting was recently merged with some open questions.

1 Like

I just keep pulling the thread and finding more confusing things :sweat_smile:

So I would expect that we only care about whether &T is Sized, and I'd expect that to always be true. It's very surprising to me MyTrait: Sized prevents making &dyn MyTrait. In C++, the fact that I have an abstract base class Base does not prevent me from doing sizeof(Derived) or sizeof(Base), and I wouldn't expect sizeof(*base_pointer) to give me sizeof(Derived) even if base_pointer = new Derived. The whole point of dyn is to assert that there's some real concrete type underneath, we just don't know which it is specifically. I guess my expectation is that &dyn Foo always guarantees the referred to object is a concrete type that satisfies Foo, but that dyn Foo is a type that satisfies everything in Foo except being Sized. I guess the crux is that there's no way to write that constraint?

Edit: this is making me realize why I see so much ?Sized usage everywhere -- generic code can't make &dyn Foo for some T if T: Sized so you have to override with T: ?Sized

The de facto way to write that constraint right now is to use another trait with a blanket impl for all Sized types, as shown above. That allows the methods from that blanket impl to be used on &dyn Trait.

Why? You are turning &Self into &dyn Trait, so Self itself needs to unsizing-coerce to dyn Trait. Thus, Self needs to be Sized.

When converting dyn Trait-the-concrete-type to dyn Trait-backed-by-dyn Trait-the-concrete type, there would currently be no place to put the vtable pointer of the backing trait object. (If you find the trait object-ception confusing, the same would apply to the length of a slice, for example.)

I don't think this has anything to do with syntactical limitation. It's a technical limitation of the implementation of fat pointers. It could be done differently (cf. the RFCs linked above by others – thanks!), and then what you want would just work. Maybe sooner or later it will.

Mind you, there's no such thing as "satisfying Trait except Sized" unless you have an explicit Sized bound on the trait. Trait declarations don't imply Sized automatically (that's just generic type parameters).

I guess that's more for supporting unsized types such as slices and str. Not being able to call File::open() with a string literal would be annoying, for example, so it takes T: ?Sized + AsRef<Path>. It's relatively rare that generic code wants to create a trait object; type erasure has its place but is likely not the main driving force behind ?Sized bounds most of the time.

After reading:

https://rust-lang.github.io/rfcs/0255-object-safety.html
https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html

I think not allowing dyn Foo when Foo: Sized is a consequence of enforcing object safety and the requirement that dyn Foo must impl Foo. I think I just find it weird because I usually think of the object safety rules as being there to enforce that you can compile a function f that calls a trait method bar once without needing to monomorphize f, but this goes beyond that. Knowing that Foo will only ever be implemented for fixed size types doesn't create a requirement to monomorphize f, or rather, doesn't make it harder to generate assembly to make virtual calls AFAICT.

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.