Trait-based enum Variants

True, I like that one, too. One thing to consider with this approach though is that you'd probably want to allow matching individual types/implementations (which are in the same compilation unit or imported by it) in an if or match statement. That way you're not limited to just the methods available on the trait (as you'd be with a normal dyn Trait (at least as far as I know). Though that information has to be in the vtable or type metadata, so doing that should already be possible using macros.

A well designed abstraction boundary should capture that boundary in the trait, and downcasting shouldn't be necessary. If downstream packages are providing the implementation, its impossible even (since you don't know the concrete types).

When possible, maintaining that type information for the downstream dependency injector should be done via generics, even if the implementation always uses polymorphic dyn dispatch internally (e.g. for code size or compilation speed benefits).

But when you do want downcasting, Any is the solution. And there are multiple crates to make working with subtraits of Any easier (since the downcast methods are defined in terms of dyn Any), such as the downcast or mopa crates (both of which are committing the same crime of assuming fat pointer layout, hooray).

Do you just mean deconstructing the wide pointer without assuming the layout (order of data and vtable pointers)? I might be corrected, but I believe there have been enough "promises" to soundly do this on stable per-trait, if the trait isn't sealed.

  • Implement the trait for a transparent wrapper of u8
  • Get ahold of a *const dyn Trait with the data at an odd address
    • e.g. from one of the contents of a a [Wrapper; 2]
  • Transmute to [*const (); 2]
  • Exactly one of these pointers will be at an odd address; that must be the data pointer
  • You now now the layout of *const dyn Trait and can pull out the vtable pointer
    • You can stash a bit in the LSB of the vtable pointer so you know how to go the other direction

Rust 1.76 will get wrapping_byte_add which will additionally eliminate the need to implement the trait for a 1-alligned struct; you'll just need a wide pointer.

Load supporting promises:


  1. implied by issue 101336 conversation, perhaps elsewhere ↩ī¸Ž

1 Like

I'm am bit late to the party, but wished to come back to this.

There is actually a benefit to having an enum beyond the jump table/if-ladder => each branch can be optimized separately. In fact, per-branch optimization potential is the whole reason for the implementation of Partial Devirtualization in GCC.

As an extreme example, imagine a case where 3 out of 4 calls to self.foo() end up calling a foo which is always inlined and does nothing. The entire dispatch can be optimized down to "is self option 4, then call its foo".

As such, it is a strictly better option than InlineBox<dyn MyTrait, ...> in the absence of Partial Devirtualization, or when for whichever reason Partial Devirtualization struggles. And that's without niche optimizations...

(With that said, I would see this more as an opaque type for impl Trait than as something you could match on... as I don't see how it could practical to gather implementations from sibling/downstream crates)

1 Like

I tend to agree with this sentiment, but ideally you could consider it a form of the exhaustiveness checking, where the opaque impl Trait, is required for the wildcard match arm. And those implementations known statically can be matched. There are definitely some issues in practice with multiple versions of types, since you can't like embed a module path in an enum variant.

So in effect, the ability to downcast via Any. You can do that today with impl 'static + Trait, actually, it just doesn't have the nicest syntax. I've thought about making a C11 _Generic macro implementation for Rust utilizing Any downcasting and/or macro specialization for a while, but never have confidently had the time and motivation to sit down and do it. Basically (untested),

fn do_stuff(thing: impl Any) {
    downcast_specialize!(thing {
        String => do_string_stuff(thing),
        _ => do_fallback_stuff(thing),
    })
}

// becomes roughly

fn do_stuff(thing: impl Any) {
    if Any::is::<String>(&thing) {
        let thing = ManuallyDrop::take(Any::downcast_mut_unchecked::<ManuallyDrop<String>>(&mut ManuallyDrop::new(thing)));
        do_string_stuff(thing)
    } else {
        do_fallback_stuff(thing)
    }
}

// plus bonus handling for by-ref-to-'static downcasting

As with any specialization, it's unsound if it allows lifetime instantiation dependent dispatch. But even limited to statically known 'static, such a utility could cover a genuine portion of specialization wants. (And type tagging approaches another good chunk.)

That would be totally impractical, I agree. But I think a partially opaque type (similar to non-exhaustive enums) should still be possible, allowing the developer to only match on dependencies he knows about (as @ratmice mentioned):

use some_other_crate::{TypeA, MyTrait};

struct TypeB {}
impl MyTrait for TypeB {}

fn example(x: enum MyTrait) {
    match x {
        TypeA => {}, // Allowed because we have TypeB in our dependency and import it
        TypeB => {}, // Allowed because we have defined TypeB
        // TypeC => {}, // Not allowed because TypeC is defined somewhere downstream (e.g. binary crate)
        _ => {}, // Required because types such as TypeC can exist
    }
}

Regarding optimization: This match statement would be similar to your self.foo() example, since all as of yet unknown types fall into the same branch and thus execute the same code. So I think with this restriction that all types used in the match branches must be importet and that there must be a catch-all branch (either panicking, doing nothing or calling one or more methods on the trait similar to how Box and InlineBox would work or using the same approach as if I call x.foo(). Though to be honest, I'm not completely sure how useful those match statements would be or how big of a performance difference it actually makes.

Something like that is what I thought about, yes.

I have to agree with your earlier statement of ideally not even needing the matching. But as demonstrated by the existence of Any it is sometimes really useful to have.

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