has a non-overridable, callable default implementation for some method, and
where that “final” method can be called from trait objects.
My attempt (also re-produced below) uses a trick described in the pre-RFC discussion of #[final] trait functions. It defines a super trait containing the final method, then provides a blanket implementation for that supertrait, preventing any other implementations from existing.
Unfortunately, I can't quite get it to fulfill all my criteria. Is there a way to achieve what I want?
pub trait TheTrait: FinalFns {
fn impl_me(&self) -> u32;
fn uses_final_fn(&self) -> bool {
self.final_fn() == 42
}
}
pub trait FinalFns /*: TheTrait*/ {
// ^^^^^^^^^^
// would introduce a cycle
fn final_fn(&self) -> u32
where
Self: TheTrait + Sized,
// Having a trait bound on `Self` removes object safety _unless_
// part of the bound is `Sized`, which prevents calls to the `final_fn`
// from trait objects.
{
self.impl_me() * 2
}
}
// blanket impl prevents existence of other impls, making the default impl final
impl<T: ?Sized> FinalFns for T {}
I think you can use a "sealed" type in the method signature to prevent downstream crates from overriding it, similar to the trick described in this blog post:
the post uses the unnamable type as argument, making the method neither callable nor implementable by downstream types. if you make it part of the return type, then it will be unimplementable, but still callable. but the downside is a little bit incovevience for the caller to extract the "true" return value.
That's a nice solution, too. I didn't pursue trait sealing because I thought the result would be inherently unusable, but with this trick, it's just mildly inconvenient to use. A good technique to have in the box!
Thanks for that variant. I can see how depending on context, the one or the other seems more “right.”
On second thought, there is one potentially significant difference. When you call dyn TheTrait as FinalFns>::final_fn(), the two versions will perform vtable lookup at different times which leads to a performance/binary size tradeoff:
@zirconium-n ’s version will look up each internal call to a TheTrait method separately, but can get away with only generating one version of final_fn.
My version will codegen a separate, statically-dispatched, version of final_fn for every type that implements TheTrait and store a pointer to that in the vtable.
Hmm I haven't really thought about difference on this part. I structure the code like that mostly to prevent impl FinalFns for SomeOtherType (without the need to seal).