Understanding trait object safety - return types

I'm trying to understand a bit more about object safety of traits. The rules definitely make sense in that they seem to care specifically about properties of the Self type and any arguments passed to the trait methods. The part I don't understand is why an object safe trait cannot have its return type specified by the caller, such as:

use num_traits::{Float, NumCast};

trait GivesFloat {
    fn give_float<T: Float + NumCast>(&self) -> Option<T>;
}

Problem is not the return type, but the fact that method is generic.

Generics in Rust are monomorphized during compilation. That means, one generic function in code would be compiled down to a whole set of functions - one for each used value of generic parameter. However, trait object have to generate virtual functions table which is the same, no matter how it is used, so it can't contain generic functions.

5 Likes

That makes sense, but I don't like it...

Though, from that it does follow that the T: Float + NumCast bound could be moved to the trait itself:

trait GivesFloat<T: Float + NumCast> {
    fn give_float(&self) -> T;
}

Thus monomorphizing the methods within the type-specified trait. Then I guess my only option would be to make separate containers for f64/f32/f16/etc-based trait objects and manage the casting between them manually. Fair enough.

And E0562 says impl Trait is not allowed outside of function and inherent method return types. Bother...

It wouldn't help here anyway, since impl Trait in return position means "function implementation chooses the return type and doesn't disclose what exactly it is", not "caller chooses the return type".

1 Like

Yes, :100: totally! Basically that way you'd be making the virtual-function-table itself generic, and then instantiating it as you were to pick a specific dyn GivesFloat.

Then, if you have a fixed list of items you'd like to yield, say f{16,32,64}, you can define a trait alias for those three capabilities specifically:

use …::f16;

trait GivesFloat<T /* : … */> {
    fn give_float(&self) -> T;
}

trait GivesAnyFloat
where
    Self : GivesFloat<f16> + GivesFloat<f32> + GivesFloat<f64>,
{}
impl<T : ?Sized> GivesAnyFloat for T
where
    Self : GivesFloat<f16> + GivesFloat<f32> + GivesFloat<f64>,
{}

fn _check (it: &'_ (dyn '_ + GivesAnyFloat))
  -> (f16, f32, f64)
{
    (it.give_float(), it.give_float(), it.give_float())
}

Basically generics, alone, are not necessarily incompatible with dyn Traits, it's just that you can't have generic associated items within a dyn Traits, since that would require an infinitely big virtual function table, which is just technically not possible.

But whenever you fix, beforehand, the possible instantiations of these generics down to a fixed (and thus, upper-bounded), set of types, you no longer have that problem, which is what we have achieved by:

  1. moving the genericity up to the trait, like you've suggested;
  2. defining a trait alias for the "sum" of the concretely desired choices of generic( capabilitie)s.
2 Likes

Yes! This is so close to what I'm hoping for. The missing bit would be the ability to re-genericize the top-level trait, but you can't do:

trait GivesAnyFloat
where
    Self: GivesFloat<T: Float>,
{}

Though I'm pretty sure from here it would be a fairly simple to use feature flags and the cfg_if crate to manage the selective application for enabled float types.