Repository navigation
RFC: implicit generic function arguments with bounds #11196
Description
Activity
Besides from the obvious ABI compatibility issue, this proposal severely limits the user's ability to selectively disable monomorphization. For example, if a function
fn f<T: Trait>(x: &T)is big enough and used with multiple differentTs, the user should be able to "refactor" this intofn f(x: &Trait)at expense of virtual method call overheads, in order to get a smaller binary.The proposal is by no means set in stone, but here's a quick way of disabling monomorphization:
fn f_virtual<T: Trait>(x: &T) { f(x as &Trait) } // Maybe this attribute could disable monomorphization by itself? // Or we could have another attribute, like #[virtual_trait_args]. #[inline(never)] fn f(x: &Trait) {...}
I'd rather have a general solution that interacts well with the rest of the language rather than a special case one (note how T, U, V remain where they are!) that doesn't. I'd look at what other languages with traits/concepts/etc do and see if it's any better. E.g. in D it is also more syntactically light to use dynamic dispatch, and yet most use static dispatch anyway. Here is what a complicated function signature with type constraints looks like in D:
void fill(Range1, Range2)(Range1 range, Range2 filler) if (isInputRange!Range1 && (isForwardRange!Range2 || (isInputRange!Range2 && isInfinite!Range2)) && is(typeof(Range1.init.front = Range2.init.front))) { }
I.e. the type constraints (the code that starts after the
if) go after the function name/arguments. C++ also has types and type bounds separate. Maybe that's a palatable solution? We even reserve thewherekeyword which could be used for the purpose:fn map2fn<T, U, V, F1, F2>(x: T, f1: F1, f2: F2) -> V where<F1: Fn<U, V>, F2: Fn<U, V>> { f2(f1(x)) } // OR fn map2fn(x: T, f1: F1, f2: F2) -> V where<T, U, V, F1: Fn<U, V>, F2: Fn<U, V>> { f2(f1(x)) }
@eddyb, re virtualisation, I believe @lifthrasiir is just pointing out that the proposed syntax only works for traits not contained in any pointers, specifically (and most importantly), you can't write
fn foo<T: Trait>(x: &T)in short hand asfn foo(x: &Trait)without some other disambiguation.Even without the speculation about
&Traitbeing optimized to<T: Trait> &T, this should still work for the non-reference version, if only to keep the same closure type syntax in some cases, if|..Args| -> Retbecomes sugar forFn<..Args, Ret>.
Is there any issue with passing a closure unboxed instead of boxed? The closure's body wouldn't actually be copied, since we already pass larger-than-register-sized objects by reference.
And I've just remembered about the awkward lifetime syntax -'a |..Args| -> Ret- with sugar that becomes&'a |..Args| -> Ret, which is just a&'a Trait.I've now written an RFC that includes something like this proposal: rust-lang/rfcs#105
This issue has been moved to the RFCs repo: rust-lang/rfcs#302
- added a commit that references this issue
on Jul 31, 2023
Using generic functions with trait bounds results in efficient code generation, at the cost of syntactic simplicity (compared to using trait objects).
Consider this snippet:
Now with trait bounds moved to argument types (creating implicit generic type parameters):
Note that this would be limited to values as references or pointers would conflict with current syntax for trait objects.
However, optimizing
&Traitin an argument type (or even a structure field? there is room for discussion here) to a generic (<T: Trait> ... &T) wouldn't cause any serious issues AFAICT.And it would mean we can make the closure syntax be sugar for
&Fnwithout losing backwards compatibility while allowing compile-time monomorphization.