Difference between impl Fn and dyn Fn

Both of these functions accept anything that implements the Fn trait but how are they different?
Also, when do you think one might use one over the other?

fn test_impl<I>(_: impl Fn(I) + 'static + Sync + Send) {}
fn test_dyn<I>(_: &(dyn Fn(I) + 'static + Sync + Send)) {}
1 Like

Both allow polymorphism, but how and when they do that is quite different. A dyn Trait trait object is a concrete type enabling dynamic dispatch at run time. You cast other concrete types to dyn Trait and then you can pass fat pointers (&dyn Trait, Box<dyn Trait>, Arc<dyn Trait>, etc.—trait objects are dynamically sized like [T]) to your functions.
impl Trait in argument position on the other hand is a form of compile time polymorphism. It's an anonymous generic type parameter. Something like fn foo(_: impl Debug) {} is pretty much syntactic sugar for fn foo<T: Debug>(_: T) {}. In a compile step called monomorphization calls to foo::<i32> and foo::<String> will lead to two distinct functions being compiled separately, avoiding any runtime dispatch overhead.

6 Likes

The first is Frankenstein. It (syntax) did not exist in Rust 1.0 but someone though it a good idea to push it on to users.

hey, thanks for the explanation.
I wanted to ask like why this doesn't work tho:

fn test<I, T: Fn(I)>(_: T + 'static + Sync + Send) {}

oh, I see

neither did dyn Fn though :slight_smile:

Addendum (in edit) for more context (as you called out in your response):


The Rust 1.0 syntax would have known

fn test_impl<I, F: Fn(I) + 'static + Sync + Send>(_: F) {}

and

fn test_dyn<I>(_: &(Fn(I) + 'static + Sync + Send)) {}

Around 2017, they decided to introduce first

fn test_impl<I>(_: impl Fn(I) + 'static + Sync + Send) {}

as (essentially) syntactic sugar for

fn test_impl<I, F: Fn(I) + 'static + Sync + Send>(_: F) {}

as an extension/complement to the at the time already accepted feature of impl Trait in return positions.


and then, a few months later[1] decided to also add

fn test_dyn<I>(_: &(dyn Fn(I) + 'static + Sync + Send)) {}

to replace

fn test_dyn<I>(_: &(Fn(I) + 'static + Sync + Send)) {}

  1. though the possibility was already considered when deciding on impl Trait ↩︎

7 Likes

Need to totally remove Frankenstein (or use a where clause)

fn test<I, T: Fn(I) + 'static + Sync + Send>(_: T) {}

I know you know but for others: polymorphism was there it just did not require(/have) dyn. Thankfully dyn is compulsory. (Was a short period where could go either with or without.)

1 Like

ah, I see. thanks!

I wanted to confirm one more thing:

fn test_dyn<I>(_: &(dyn Fn(I) + 'static + Sync + Send)) {}

this means that the erased type should only have 'static lifetime (if appears any) and the erased type should also be Send and Sync, is that correct?

Yes that sounds correct.

1 Like

Another thing to keep in mind is that if the function accepts impl Fn(I) then the caller can pass &dyn Fn(I) to it, because &dyn Fn(I) implements Fn(I). So, impl Fn(I) (or <F: Fn(I)>) is the most flexible option for the caller — they get to choose dynamic or static dispatch.

5 Likes

Another difference is that I is part of dyn Fn(I) + ...'s type which participates in outlives bounds independently of the dyn lifetime, so the dyn type may not be 'static even though the erased type was (modulo unsafe).

fn assert_static<T: ?Sized + 'static>(_: &T) {}

// Works
fn test_impl<I, F: Fn(I) + 'static + Sync + Send>(f: F) {
    assert_static(&f)
}

// error[E0310]: the parameter type `I` may not live long enough
fn test_dyn<I>(f: &(dyn Fn(I) + 'static + Sync + Send)) {
    assert_static(f)
}

Another difference is that a &dyn _ parameter may be dyn compatible when a generic parameter would not be (although in the OP I is a separate generic anyway).

3 Likes

Hello, thank you everyone for sharing different points and things to keep in mind : )

it makes sense for something like this to be + 'static and the erased type could be 'static str or a String

&(dyn Trait + 'static + Sync + Send)

But here how can a lifetime even appear in a closure's type....?

fn test_dyn<I>(_: &(dyn Fn(I) + 'static + Sync + Send)) {}

It might be (immutably) borrowing a Mutex from an outer scope, for instance.

Maybe I is a &'non_static str or whatever.

An implementor being 'static when the parameter is not 'static doesn't come up often, but can in niche circumstances.

Even if it didn't, it would be unsound for the dyn parameter to not participate in outlives bounds as the language is today, as that would permit transforming from dyn FnMut(&'static str) to dyn FnMut(&'whatever str) via dyn Any (for example).

1 Like

It's a matter of editions, not periods. Older editions still allow omitting dyn, even on newer compilers.