Search code examples
rusttrait-objects

Why does Rust allow you to call `Iterator::for_each()` on a trait object?


I was playing around with some API concepts and noticed something peculiar in Rust's Iterator trait.

I have the following trait definition:

trait Observable {
    type Item;

    fn subscribe<F>(self, f: F) -> bool
    where
        Self: Sized,
        F: FnMut(Self::Item) + 'static;
}

I then proceeded to write the following test:

#[test]
#[should_panic]
fn trait_obj() {
    let mut v: Vec<Box<dyn Iterator<Item = ()>>> = vec![];
    let mut v2: Vec<Box<dyn Observable<Item = Ref<u8>>>> = vec![];

    v.remove(0).for_each(|_| {});
    v2.remove(0).subscribe(|_| {});
}

The above test does not compile, as one would expect; subscribe() takes self by value, and has a Sized constraint on Self, therefore is not object safe. However, if I comment out the ...subscribe line, it does compile!

The odd thing to me is, Iterator::for_each() has the same constraints. Why is this allowed for Iterator and not for Observable? Is it an experimental feature that enables this?

Here is the function signature of Iterator::for_each for reference:

// Iterator::for_each
fn for_each<F>(self, f: F)
where
    Self: Sized,
    F: FnMut(Self::Item);

The function signatures for Iterator::for_each and Observable::subscribe are pretty much identical.

What gives?


Solution

  • Your mistake is that you think you call <dyn Iterator<Item = ()>>::for_each(), and then you wonder rightfully how you can if for_each() requires Self: Sized but dyn Iterator<Item = ()> is obviously not Sized. But you are wrong. And you can see that if you'll use UFCS (Universal Function Call Syntax):

    #[test]
    #[should_panic]
    fn trait_obj() {
        let mut v: Vec<Box<dyn Iterator<Item = ()>>> = vec![];
    
        // v.remove(0).for_each(|_| {});
        <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
    }
    

    Playground.

    Emits:

    error[E0277]: the size for values of type `dyn Iterator<Item = ()>` cannot be known at compilation time
       --> src/lib.rs:7:5
        |
    7   |     <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
        |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
        |
        = help: the trait `Sized` is not implemented for `dyn Iterator<Item = ()>`
    note: required by a bound in `for_each`
    
    error[E0308]: mismatched types
     --> src/lib.rs:7:41
      |
    7 |     <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
      |                                         ^^^^^^^^^^^ expected trait object `dyn Iterator`, found struct `Box`
      |
      = note: expected trait object `dyn Iterator<Item = ()>`
                       found struct `Box<dyn Iterator<Item = ()>>`
    help: consider unboxing the value
      |
    7 |     <dyn Iterator<Item = ()>>::for_each(*v.remove(0), |_| {});
      |                                         +
    
    error[E0277]: the size for values of type `dyn Iterator<Item = ()>` cannot be known at compilation time
     --> src/lib.rs:7:41
      |
    7 |     <dyn Iterator<Item = ()>>::for_each(v.remove(0), |_| {});
      |                                         ^^^^^^^^^^^ doesn't have a size known at compile-time
      |
      = help: the trait `Sized` is not implemented for `dyn Iterator<Item = ()>`
      = note: all function arguments must have a statically known size
    

    And this error also hints you why the previous version worked: you didn't call <dyn Iterator<Item = ()>>::for_each(), you called Box::<dyn Iterator<Item = ()>>::for_each()! Box<Iterator> implements Iterator itself, and so it worked. You can see that explicitly in the MIR:

    v.remove(0).for_each(|_| {});
    
    // Snip
    _2 = <Box<dyn Iterator<Item = ()>> as Iterator>::for_each::<[closure@src/lib.rs:4:26: 4:32]>(move _3, move _5) -> [return: bb3, unwind: bb5];
    // Snip
    

    Playground (choose "Show MIR" from the menu).

    If you has had implemented Observable for Box<O> where O: Observable, it would work for you too...

    ...Except you cannot. Because you cannot forward the call to for_each(). The reason it works with Iterator is that it does not forward this call, and rather uses the default implementation that calls next() again and again. And because next() takes &mut self, it doesn't require Self: Sized.