Skip to content

← Generics and Traits step 18 of 24

Hard Primitives

Returning impl Trait, and what edition 2024 changed

-> impl Trait in return position (RPIT) is how you hand back a closure or an iterator chain without boxing it. The type is real and concrete — the compiler knows exactly which Filter<Iter<'_, i64>, {closure}> you built — but the caller only sees the promise. No allocation, no vtable, full inlining.

It is also where the most confusing edition difference in Rust lives, and where a large fraction of the tutorials you will find online are now wrong.

Your task

Three functions, one of them already correct:

pub fn evens(v: &[i64]) -> impl Iterator<Item = &i64>
pub fn boxed_evens(v: &[i64]) -> Box<dyn Iterator<Item = &i64> + '_>
pub fn picker(want_even: bool) -> Box<dyn Fn(i64) -> bool>

pub fn run(nums: Vec<i64>, want_even: bool) -> Vec<i64>

run concatenates three lists: the evens via evens, the evens again via boxed_evens, and then every element matching the closure picker returned. For [1,2,3,4] with want_even = true that is [2,4, 2,4, 2,4]; with want_even = false it is [2,4, 2,4, 1,3].

run is written for you. Fix the two broken signatures.

(Note the harness constraint: the graded function cannot itself return impl Iterator, because main has to serialise the result to JSON. The RPIT functions are internal helpers behind a Vec-returning entry point — which is how you would structure real code anyway.)

Broken signature 1: the asymmetry

This compiles:

pub fn evens(v: &[i64]) -> impl Iterator<Item = &i64>

This does not:

pub fn boxed_evens(v: &[i64]) -> Box<dyn Iterator<Item = &i64>>
error: lifetime may not live long enough
   |
   | returning this value requires that `'1` must outlive `'static`
help: to declare that the trait object captures data from argument `v`,
      you can add an explicit `'_` lifetime bound
   | pub fn boxed_evens(v: &[i64]) -> Box<dyn Iterator<Item = &i64> + '_> {
   |                                                                ++++

The asymmetry is the entire teaching point. These are two different defaulting rules that happen to sit next to each other:

  • An RPIT captures every in-scope generic parameter, lifetimes included, automatically. evens needs nothing.
  • A trait object in a Box defaults to + 'static unless you write a lifetime. boxed_evens must say + '_.

Learners over-generalise in both directions — either sprinkling '_ onto RPITs where it is now redundant, or omitting it from Box<dyn ...> where it is still required. They are separate rules. Memorise them separately.

Broken signature 2: one return type, not two

pub fn picker(want_even: bool) -> impl Fn(i64) -> bool {
    if want_even { |n: i64| n % 2 == 0 } else { |n: i64| n % 2 != 0 }
}
error[E0308]: `if` and `else` have incompatible types
   = note: no two closures, even if identical, have the same type

impl Trait is not a trait object. It means “one specific type that I am not naming”, and two closure literals are two different types no matter how alike they look. The fix is to erase them to a common type: Box<dyn Fn(i64) -> bool>, with each branch boxed. You pay one allocation and one indirect call; in exchange the signature becomes expressible.

The neighbouring error worth recognising: writing -> dyn Fn(i64) -> bool with no Box gives E0746, “return type cannot have an unboxed trait object”. dyn Trait is unsized, and Rust returns values on the stack.

The edition 2024 capture change

This is the part that makes old tutorials wrong.

In edition 2021, an RPIT captured a lifetime only if that lifetime appeared syntactically in the impl Trait bound. So

fn f(_: &()) -> impl Sized {}

meant “captures nothing” — in today’s notation, + use<>. That is why 2021 code is full of + '_ annotations on iterator-returning functions: without them the compiler refused to let the returned value borrow from the argument.

In edition 2024 all in-scope generic parameters, including lifetimes, are captured unconditionally. The same signature now means + use<'_>. This is why evens above needs no annotation, and why a tutorial telling you to add + '_ is describing a language that no longer exists.

You can see the change directly. This compiled in 2021 and is an error in 2024:

fn test<'a>(x: &'a ()) -> impl Sized + 'static { capture(x) }

In 2021 'a was not captured, so the result really could be 'static. In 2024 'a is captured, so it cannot.

Since Rust 1.82 there is explicit control, use<..>:

fn g<'a, T>(x: &'a T) -> impl Sized + use<'a, T> { ... }   // capture these
fn h(x: &()) -> impl Sized + use<> { }                     // capture nothing

Two lints exist for this, impl_trait_overcaptures and impl_trait_redundant_captures, and both are rustc allow-by-default — they will not fire unless you turn them on. If you want to see them, put #![warn(impl_trait_overcaptures)] at the very top of your submission; your code is the first thing in the compilation unit, so a crate-level attribute there is legal.

One thing not to over-generalise: type and const parameters were always captured, in every edition. Only lifetimes changed. People read the changelog and conclude the whole capture story is new; it is not.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…