Skip to content

← Generics and Traits step 18 of 24

Hard Primitives

Returning impl Trait, and what edition 2024 changed

Compare an iterator returned through impl Trait with one returned through a boxed trait object. Repair the boxed iterator's lifetime and practice returning a boxed predicate.

Implement these helper signatures while keeping the supplied run function:

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 groups in this order:

  1. Even values selected by evens.
  2. The same even values selected by boxed_evens.
  3. Values matching picker: even when want_even is true, odd otherwise.

Preserve order and duplicates within every group. Zero is even; negative values follow the same parity rule. Empty input produces empty output. For [1, 2, 3, 4], the result is [2, 4, 2, 4, 2, 4] when selecting evens and [2, 4, 2, 4, 1, 3] when selecting odds.

The harness serializes the vector returned by run. The helpers demonstrate return-type choices behind that entry point. Output tests alone cannot distinguish all equivalent helper signatures; use the requested signatures to practice the API design.

A hidden concrete type versus a trait object

Return-position impl Trait, often shortened to RPIT, hides the name of a concrete return type. evens returns a particular iterator adapter type. The signature itself does not require boxing or dynamic dispatch, but it does not guarantee that arbitrary implementations allocate nothing or that every call is inlined.

Box<dyn Iterator<...>> instead uses a trait object. In this return type, omitting the object's lifetime bound defaults it to 'static. The iterator borrows v, so add + '_ to tie that object bound to the input lifetime:

pub fn boxed_evens(v: &[i64]) -> Box<dyn Iterator<Item = &i64> + '_> {
    Box::new(v.iter().filter(|n| *n % 2 == 0))
}

The lifetime of the referenced Item and the trait object's own lifetime bound are separate parts of the type. The former does not automatically replace the latter. Trait-object defaults also depend on context; do not generalize this particular boxed return type to every dyn Trait use.

The starter's predicate already compiles

Each closure expression has its own type, but these two non-capturing closures can coerce to a common function-pointer type:

pub fn picker(want_even: bool) -> impl Fn(i64) -> bool {
    if want_even {
        |n: i64| n % 2 == 0
    } else {
        |n: i64| n % 2 != 0
    }
}

This is valid Rust. For this exercise, change the return type to the requested Box<dyn Fn(i64) -> bool> and box each branch. That demonstrates type erasure; it is not the only way to express this operation.

A single closure capturing want_even is another possible design. In general, RPIT requires one concrete return type, so distinct capturing closure types may need restructuring or a shared wrapper.

Boxing does not always allocate: these particular closures capture nothing and have zero-sized environments. Do not assume one heap allocation per Box::new without considering the value's size. Trait-object calls permit dynamic dispatch, though optimization may remove it in some contexts.

What edition 2024 changes

evens is valid in both editions 2021 and 2024. Its input lifetime already appears in the returned iterator's Item = &i64 bound through elision. It is not an example of the edition change.

For ordinary free functions without an explicit capture list, edition 2021 captures lifetimes mentioned in the RPIT bounds. Edition 2024 captures all in-scope generic lifetimes. Type and const parameters were already captured. Trait-method RPIT and async functions had broader capture rules before 2024.

Here is a complete example where the edition matters:

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

fn independent(x: &()) -> impl Sized + 'static {
    marker(x)
}

It compiles in edition 2021. In edition 2024, marker's opaque return type captures the input lifetime, so independent cannot promise a static result for an arbitrary input borrow. This happens even though marker returns unit and stores no reference. Capture affects the public opaque type's contract, not just whether its current implementation contains a pointer.

Use a precise capture list when that dependency is unwanted:

fn marker(_: &()) -> impl Sized + use<> {}

fn independent(x: &()) -> impl Sized + 'static {
    marker(x)
}

This version works in both editions. use<> captures no generic parameters. A nonempty list names the parameters the opaque type may capture; it does not extend any borrowed storage's lifetime. Precise capture lists have restrictions, including listing all in-scope type and const parameters. An outlives bound such as + 'a expresses a different promise from a capture permission.

Keep evens as supplied, repair boxed_evens, and change picker to the requested boxed API. Passing requires compilation, correct results and Clippy with warnings treated as errors.