Skip to content

Commit 3ac89d3

Browse files
committed
Rewrite the dynamic dispatch section to focus on usage rather than implementation.
1 parent 638832e commit 3ac89d3

File tree

1 file changed

+68
-49
lines changed

1 file changed

+68
-49
lines changed

src/doc/trpl/static-and-dynamic-dispatch.md

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -102,49 +102,88 @@ reason.
102102
Rust provides dynamic dispatch through a feature called 'trait objects.' Trait
103103
objects, like `&Foo` or `Box<Foo>`, are normal values that store a value of
104104
*any* type that implements the given trait, where the precise type can only be
105-
known at runtime. The methods of the trait can be called on a trait object via
106-
a special record of function pointers (created and managed by the compiler).
105+
known at runtime.
107106

108-
A function that takes a trait object is not specialized to each of the types
109-
that implements `Foo`: only one copy is generated, often (but not always)
110-
resulting in less code bloat. However, this comes at the cost of requiring
111-
slower virtual function calls, and effectively inhibiting any chance of
112-
inlining and related optimisations from occurring.
107+
A trait object can be obtained from a pointer to a concrete type that
108+
implements the trait by *casting* it (e.g. `&x as &Foo`) or *coercing* it
109+
(e.g. using `&x` as an argument to a function that takes `&Foo`).
113110

114-
Trait objects are both simple and complicated: their core representation and
115-
layout is quite straight-forward, but there are some curly error messages and
116-
surprising behaviors to discover.
111+
These trait object coercions and casts also work for pointers like `&mut T` to
112+
`&mut Foo` and `Box<T>` to `Box<Foo>`, but that's all at the moment. Coercions
113+
and casts are identical.
117114

118-
### Obtaining a trait object
115+
This operation can be seen as "erasing" the compiler's knowledge about the
116+
specific type of the pointer, and hence trait objects are sometimes referred to
117+
as "type erasure".
119118

120-
There's two similar ways to get a trait object value: casts and coercions. If
121-
`T` is a type that implements a trait `Foo` (e.g. `u8` for the `Foo` above),
122-
then the two ways to get a `Foo` trait object out of a pointer to `T` look
123-
like:
119+
Coming back to the example above, we can use the same trait to perform dynamic
120+
dispatch with trait objects by casting:
124121

125-
```{rust,ignore}
126-
let ref_to_t: &T = ...;
122+
```rust
123+
# trait Foo { fn method(&self) -> String; }
124+
# impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } }
125+
# impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } }
127126

128-
// `as` keyword for casting
129-
let cast = ref_to_t as &Foo;
127+
fn do_something(x: &Foo) {
128+
x.method();
129+
}
130130

131-
// using a `&T` in a place that has a known type of `&Foo` will implicitly coerce:
132-
let coerce: &Foo = ref_to_t;
131+
fn main() {
132+
let x = 5u8;
133+
do_something(&x as &Foo);
134+
}
135+
```
133136

134-
fn also_coerce(_unused: &Foo) {}
135-
also_coerce(ref_to_t);
137+
or by coercing:
138+
139+
```rust
140+
# trait Foo { fn method(&self) -> String; }
141+
# impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } }
142+
# impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } }
143+
144+
fn do_something(x: &Foo) {
145+
x.method();
146+
}
147+
148+
fn main() {
149+
let x = "Hello".to_string();
150+
do_something(&x);
151+
}
136152
```
137153

138-
These trait object coercions and casts also work for pointers like `&mut T` to
139-
`&mut Foo` and `Box<T>` to `Box<Foo>`, but that's all at the moment. Coercions
140-
and casts are identical.
154+
A function that takes a trait object is not specialized to each of the types
155+
that implements `Foo`: only one copy is generated, often (but not always)
156+
resulting in less code bloat. However, this comes at the cost of requiring
157+
slower virtual function calls, and effectively inhibiting any chance of
158+
inlining and related optimisations from occurring.
141159

142-
This operation can be seen as "erasing" the compiler's knowledge about the
143-
specific type of the pointer, and hence trait objects are sometimes referred to
144-
as "type erasure".
160+
### Why pointers?
161+
162+
Rust does not put things behind a pointer by default, unlike many managed
163+
languages, so types can have different sizes. Knowing the size of the value at
164+
compile time is important for things like passing it as an argument to a
165+
function, moving it about on the stack and allocating (and deallocating) space
166+
on the heap to store it.
167+
168+
For `Foo`, we would need to have a value that could be at least either a
169+
`String` (24 bytes) or a `u8` (1 byte), as well as any other type for which
170+
dependent crates may implement `Foo` (any number of bytes at all). There's no
171+
way to guarantee that this last point can work if the values are stored without
172+
a pointer, because those other types can be arbitrarily large.
173+
174+
Putting the value behind a pointer means the size of the value is not relevant
175+
when we are tossing a trait object around, only the size of the pointer itself.
145176

146177
### Representation
147178

179+
The methods of the trait can be called on a trait object via a special record
180+
of function pointers traditionally called a 'vtable' (created and managed by
181+
the compiler).
182+
183+
Trait objects are both simple and complicated: their core representation and
184+
layout is quite straight-forward, but there are some curly error messages and
185+
surprising behaviors to discover.
186+
148187
Let's start simple, with the runtime representation of a trait object. The
149188
`std::raw` module contains structs with layouts that are the same as the
150189
complicated built-in types, [including trait objects][stdraw]:
@@ -265,23 +304,3 @@ let y = TraitObject {
265304
If `b` or `y` were owning trait objects (`Box<Foo>`), there would be a
266305
`(b.vtable.destructor)(b.data)` (respectively `y`) call when they went out of
267306
scope.
268-
269-
### Why pointers?
270-
271-
The use of language like "fat pointer" implies that a trait object is
272-
always a pointer of some form, but why?
273-
274-
Rust does not put things behind a pointer by default, unlike many managed
275-
languages, so types can have different sizes. Knowing the size of the value at
276-
compile time is important for things like passing it as an argument to a
277-
function, moving it about on the stack and allocating (and deallocating) space
278-
on the heap to store it.
279-
280-
For `Foo`, we would need to have a value that could be at least either a
281-
`String` (24 bytes) or a `u8` (1 byte), as well as any other type for which
282-
dependent crates may implement `Foo` (any number of bytes at all). There's no
283-
way to guarantee that this last point can work if the values are stored without
284-
a pointer, because those other types can be arbitrarily large.
285-
286-
Putting the value behind a pointer means the size of the value is not relevant
287-
when we are tossing a trait object around, only the size of the pointer itself.

0 commit comments

Comments
 (0)