Rust Trait Objects and Dispatch: dyn Trait vs impl Trait¶
Use generics when an operation can keep its concrete implementation type, and a trait object when values with different implementation types must share one runtime interface. Argument-position impl Trait is generic syntax; return-position impl Trait hides one concrete type rather than creating a mixed-type container.
Prerequisites and outcome¶
Complete lifetimes and borrowed APIs. You will compare three calling forms, build a borrowed heterogeneous collection, and recognise restrictions on dyn-compatible traits. The examples use fixture readings; no sensor or performance measurement is needed.
Choose the Rust interface by what must vary¶
| Form | Concrete implementation choice | Useful when |
|---|---|---|
T: Read |
Caller chooses T; each instantiation keeps that type | Generic algorithms and relationships between arguments |
Argument impl Read |
Caller supplies an anonymous generic type | One parameter needs a simple behaviour bound |
Return impl Read |
Function hides one concrete return type | Exposing behaviour without naming the implementation |
&dyn Read<Value = i32> |
A borrowed trait object carries an implementation at runtime | Mixed concrete implementations behind one interface |
The associated type is specified in the object type because the caller still needs to know the read method's return type. This example is deliberately narrower than the previous Source trait: Read has no associated constant, so it can be used as a trait object.
Build the complete dispatch comparison¶
Keep edition = "2024" in Cargo.toml and replace src/main.rs with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
Expected output:
There are seven tests. Zero is a present value, not absence, regardless of the calling form. The object array borrows its sources; they must remain valid while those references are used.
Static and dynamic dispatch do not choose ownership¶
The generic function is instantiated for its concrete uses. The impl-argument form is also generic; it does not erase the type through a vtable. Naming T is useful when the same type must appear in several parameters or the return type; separate impl Trait parameters do not assert that their types are equal. Changing public signatures can also change which explicit generic arguments callers may supply.
A trait-object pointer carries access to the value and its implementation's vtable; object calls use dynamic dispatch. This is a language-level description, not a promise of a particular ABI or machine instruction after optimisation. See the trait-object reference.
Both our generic and dynamic functions borrow. Passing a value by a generic T could instead move it, while Box owns its contained value. The keyword dyn neither forces a heap allocation nor makes data thread-safe. &dyn Read in the array needs no boxing; the boxed_borrow function uses Box only to demonstrate an owned wrapper. Box's resource behaviour is taught later.
dyn Read<Value = i32> is dynamically sized. The ?Sized bound allows read_generic to accept an object through &T; removing it restores the implicit Sized requirement. The impl-argument function shown here retains that requirement, so the two signatures do not accept exactly the same set of inputs.
Opaque return types keep one concrete type¶
make_fixed returns Fixed while promising only Read
The impl Trait reference distinguishes argument parameters from opaque returns. In Rust 2024, opaque returns automatically capture in-scope generic parameters, including lifetimes. A use<...> bound can precisely control permitted captures where supported. Our factory takes an owned integer and needs no borrowed capture; borrowing factories and edition differences require attention rather than assuming impl Trait means 'static.
If runtime branches must return different implementations, use a suitable enum to keep a closed set of alternatives, or a boxed dyn-compatible interface when runtime type erasure fits the API. Neither is universally the faster or simpler choice. We make no performance comparison here.
Dyn compatibility is a trait design constraint¶
Our Read has an ordinary associated type fixed in the object type, and a method with an &self receiver. That is sufficient for this example. The Source trait from lesson 12 has an associated constant and is not dyn-compatible as written.
Other restrictions include a Sized supertrait, generic associated types, and dispatchable methods with type parameters or unsupported uses of Self. A method that needs concrete Self can sometimes be marked where Self: Sized; it then cannot be called on the trait object. Generic methods, opaque returns and async methods must be checked against the actual rules, not accepted just because other methods work. Consult dyn compatibility; older material calls it object safety.
Auto-trait bounds such as Send or Sync can further restrict an object, but the implementation must satisfy them. A trait object cannot simply combine arbitrary unrelated non-auto traits with plus; a named supertrait can expose a combined interface. Concurrency requirements are covered in their own lessons.
Object lifetimes are separate from pointer ownership¶
Box<dyn Read<Value = i32> + 'a> allows the erased implementation to contain a reference valid for 'a. The Box owns Borrowed, while the local value remains owned by main. Dropping the Box releases its wrapper, not the integer's owner.
Outside expression inference, a plain Box
Deliberately failing: two hidden concrete return types¶
In a separate scratch project, replace src/main.rs with:
cargo check reports E0308. Both types implement Read, but an opaque return is not a runtime union. A repair is to return Box
Exercises and troubleshooting¶
- Remove ?Sized from read_generic and call it with a &dyn Read
: expect E0277. Restore it when borrowing unsized implementations is intended. - Try an array
[Fixed(0), Unavailable]without trait-object references: expect E0308 because array elements need one type. - Add an associated constant with a default to Read: expect E0038 at the trait-object uses. Remove it from this runtime interface or redesign the interface; do not delete useful constants from unrelated traits merely to make everything dyn-compatible.
- Remove
+ 'afrom boxed_borrow's return type: expect a lifetime error because the default object bound cannot accept this local borrow. Restore the explicit lifetime; do not leak the integer. - Repair choose with Box
and Box::new in both branches. Test both true and false: expect Some(46700) and None. - Give make_fixed two branches that both construct Fixed with different integers: it remains a valid impl Read return because the concrete type is unchanged.
When a compiler error says a trait is not dyn-compatible, inspect its associated items. For mismatched opaque returns, inspect concrete branch types. For a lifetime error, inspect the erased implementation's borrows and the object bound, not only whether the outer pointer owns its wrapper.
Verification and next step¶
On October 10, 2026, the lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, all seven tests, formatting and debug/release output comparisons passed. The boxed branch repair returned both present and missing results; same-concrete-type opaque branches also passed. Mixed concrete array elements, incompatible opaque returns, a missing relaxed Sized bound, an associated constant on the object trait and an omitted borrowed-object lifetime failed as expected (E0308, E0277, E0038 and a lifetime diagnostic as applicable). No live sensor or dispatch benchmark is claimed.
Next: closures and function pointers, choosing capture ownership and callable bounds.