Skip to content

Commit dbccd70

Browse files
committed
Add documentation on trait objects.
Largely taken from @huonw's http://huonw.github.io/blog/2015/01/peeking-inside-trait-objects/ Fixes #21707
1 parent 012e964 commit dbccd70

File tree

3 files changed

+289
-45
lines changed

3 files changed

+289
-45
lines changed

src/doc/trpl/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* [Iterators](iterators.md)
2828
* [Generics](generics.md)
2929
* [Traits](traits.md)
30+
* [Static and Dynamic Dispatch](static-and-dynamic-dispatch.md)
3031
* [Concurrency](concurrency.md)
3132
* [Error Handling](error-handling.md)
3233
* [Documentation](documentation.md)
+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
% Static and Dynamic Dispatch
2+
3+
When code involves polymorphism, there needs to be a mechanism to determine
4+
which specific version is actually run. This is called 'dispatch.' There are
5+
two major forms of dispatch: static dispatch and dynamic dispatch. While Rust
6+
favors static dispatch, it also supports dynamic dispatch through a mechanism
7+
called 'trait objects.'
8+
9+
## Background
10+
11+
For the rest of this chapter, we'll need a trait and some implementations.
12+
Let's make a simple one, `Foo`. It has one method that is expected to return a
13+
`String`.
14+
15+
```rust
16+
trait Foo {
17+
fn method(&self) -> String;
18+
}
19+
```
20+
21+
We'll also implement this trait for `u8` and `String`:
22+
23+
```rust
24+
# trait Foo { fn method(&self) -> String; }
25+
impl Foo for u8 {
26+
fn method(&self) -> String { format!("u8: {}", *self) }
27+
}
28+
29+
impl Foo for String {
30+
fn method(&self) -> String { format!("string: {}", *self) }
31+
}
32+
```
33+
34+
35+
## Static dispatch
36+
37+
We can use this trait to perform static dispatch with trait bounds:
38+
39+
```rust
40+
# trait Foo { fn method(&self) -> String; }
41+
# impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } }
42+
# impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } }
43+
fn do_something<T: Foo>(x: T) {
44+
x.method();
45+
}
46+
47+
fn main() {
48+
let x = 5u8;
49+
let y = "Hello".to_string();
50+
51+
do_something(x);
52+
do_something(y);
53+
}
54+
```
55+
56+
Rust uses 'monomorphization' to perform static dispatch here. This means that
57+
Rust will create a special version of `do_something()` for both `u8` and
58+
`String`, and then replace the call sites with calls to these specialized
59+
functions. In other words, Rust generates something like this:
60+
61+
```rust
62+
# trait Foo { fn method(&self) -> String; }
63+
# impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } }
64+
# impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } }
65+
fn do_something_u8(x: u8) {
66+
x.method();
67+
}
68+
69+
fn do_something_string(x: String) {
70+
x.method();
71+
}
72+
73+
fn main() {
74+
let x = 5u8;
75+
let y = "Hello".to_string();
76+
77+
do_something_u8(x);
78+
do_something_string(y);
79+
}
80+
```
81+
82+
This has some upsides: static dispatching of any method calls, allowing for
83+
inlining and hence usually higher performance. It also has some downsides:
84+
causing code bloat due to many copies of the same function existing in the
85+
binary, one for each type.
86+
87+
Furthermore, compilers aren’t perfect and may “optimise” code to become slower.
88+
For example, functions inlined too eagerly will bloat the instruction cache
89+
(cache rules everything around us). This is part of the reason that `#[inline]`
90+
and `#[inline(always)]` should be used carefully, and one reason why using a
91+
dynamic dispatch is sometimes more efficient.
92+
93+
However, the common case is that it is more efficient to use static dispatch,
94+
and one can always have a thin statically-dispatched wrapper function that does
95+
a dynamic, but not vice versa, meaning static calls are more flexible. The
96+
standard library tries to be statically dispatched where possible for this
97+
reason.
98+
99+
## Dynamic dispatch
100+
101+
Rust provides dynamic dispatch through a feature called 'trait objects.' Trait
102+
objects, like `&Foo` or `Box<Foo>`, are normal values that store a value of
103+
*any* type that implements the given trait, where the precise type can only be
104+
known at runtime. The methods of the trait can be called on a trait object via
105+
a special record of function pointers (created and managed by the compiler).
106+
107+
A function that takes a trait object is not specialised to each of the types
108+
that implements `Foo`: only one copy is generated, often (but not always)
109+
resulting in less code bloat. However, this comes at the cost of requiring
110+
slower virtual function calls, and effectively inhibiting any chance of
111+
inlining and related optimisations from occurring.
112+
113+
Trait objects are both simple and complicated: their core representation and
114+
layout is quite straight-forward, but there are some curly error messages and
115+
surprising behaviours to discover.
116+
117+
### Obtaining a trait object
118+
119+
There's two similar ways to get a trait object value: casts and coercions. If
120+
`T` is a type that implements a trait `Foo` (e.g. `u8` for the `Foo` above),
121+
then the two ways to get a `Foo` trait object out of a pointer to `T` look
122+
like:
123+
124+
```{rust,ignore}
125+
let ref_to_t: &T = ...;
126+
127+
// `as` keyword for casting
128+
let cast = ref_to_t as &Foo;
129+
130+
// using a `&T` in a place that has a known type of `&Foo` will implicitly coerce:
131+
let coerce: &Foo = ref_to_t;
132+
133+
fn also_coerce(_unused: &Foo) {}
134+
also_coerce(ref_to_t);
135+
```
136+
137+
These trait object coercions and casts also work for pointers like `&mut T` to
138+
`&mut Foo` and `Box<T>` to `Box<Foo>`, but that's all at the moment. Coercions
139+
and casts are identical.
140+
141+
This operation can be seen as "erasing" the compiler's knowledge about the
142+
specific type of the pointer, and hence trait objects are sometimes referred to
143+
"type erasure".
144+
145+
### Representation
146+
147+
Let's start simple, with the runtime representation of a trait object. The
148+
`std::raw` module contains structs with layouts that are the same as the
149+
complicated build-in types, [including trait objects][stdraw]:
150+
151+
```rust
152+
# mod foo {
153+
pub struct TraitObject {
154+
pub data: *mut (),
155+
pub vtable: *mut (),
156+
}
157+
# }
158+
```
159+
160+
[stdraw]: ../std/raw/struct.TraitObject.html
161+
162+
That is, a trait object like `&Foo` consists of a "data" pointer and a "vtable"
163+
pointer.
164+
165+
The data pointer addresses the data (of some unknown type `T`) that the trait
166+
object is storing, and the vtable pointer points to the vtable ("virtual method
167+
table") corresponding to the implementation of `Foo` for `T`.
168+
169+
170+
A vtable is essentially a struct of function pointers, pointing to the concrete
171+
piece of machine code for each method in the implementation. A method call like
172+
`trait_object.method()` will retrieve the correct pointer out of the vtable and
173+
then do a dynamic call of it. For example:
174+
175+
```{rust,ignore}
176+
struct FooVtable {
177+
destructor: fn(*mut ()),
178+
size: usize,
179+
align: usize,
180+
method: fn(*const ()) -> String,
181+
}
182+
183+
// u8:
184+
185+
fn call_method_on_u8(x: *const ()) -> String {
186+
// the compiler guarantees that this function is only called
187+
// with `x` pointing to a u8
188+
let byte: &u8 = unsafe { &*(x as *const u8) };
189+
190+
byte.method()
191+
}
192+
193+
static Foo_for_u8_vtable: FooVtable = FooVtable {
194+
destructor: /* compiler magic */,
195+
size: 1,
196+
align: 1,
197+
198+
// cast to a function pointer
199+
method: call_method_on_u8 as fn(*const ()) -> String,
200+
};
201+
202+
203+
// String:
204+
205+
fn call_method_on_String(x: *const ()) -> String {
206+
// the compiler guarantees that this function is only called
207+
// with `x` pointing to a String
208+
let string: &String = unsafe { &*(x as *const String) };
209+
210+
string.method()
211+
}
212+
213+
static Foo_for_String_vtable: FooVtable = FooVtable {
214+
destructor: /* compiler magic */,
215+
// values for a 64-bit computer, halve them for 32-bit ones
216+
size: 24,
217+
align: 8,
218+
219+
method: call_method_on_String as fn(*const ()) -> String,
220+
};
221+
```
222+
223+
The `destructor` field in each vtable points to a function that will clean up
224+
any resources of the vtable's type, for `u8` it is trivial, but for `String` it
225+
will free the memory. This is necessary for owning trait objects like
226+
`Box<Foo>`, which need to clean-up both the `Box` allocation and as well as the
227+
internal type when they go out of scope. The `size` and `align` fields store
228+
the size of the erased type, and its alignment requirements; these are
229+
essentially unused at the moment since the information is embedded in the
230+
destructor, but will be used in future, as trait objects are progressively made
231+
more flexible.
232+
233+
Suppose we've got some values that implement `Foo`, the explicit form of
234+
construction and use of `Foo` trait objects might look a bit like (ignoring the
235+
type mismatches: they're all just pointers anyway):
236+
237+
```{rust,ignore}
238+
let a: String = "foo".to_string();
239+
let x: u8 = 1;
240+
241+
// let b: &Foo = &a;
242+
let b = TraitObject {
243+
// store the data
244+
data: &a,
245+
// store the methods
246+
vtable: &Foo_for_String_vtable
247+
};
248+
249+
// let y: &Foo = x;
250+
let y = TraitObject {
251+
// store the data
252+
data: &x,
253+
// store the methods
254+
vtable: &Foo_for_u8_vtable
255+
};
256+
257+
// b.method();
258+
(b.vtable.method)(b.data);
259+
260+
// y.method();
261+
(y.vtable.method)(y.data);
262+
```
263+
264+
If `b` or `y` were owning trait objects (`Box<Foo>`), there would be a
265+
`(b.vtable.destructor)(b.data)` (respectively `y`) call when they went out of
266+
scope.
267+
268+
### Why pointers?
269+
270+
The use of language like "fat pointer" implies that a trait object is
271+
always a pointer of some form, but why?
272+
273+
Rust does not put things behind a pointer by default, unlike many managed
274+
languages, so types can have different sizes. Knowing the size of the value at
275+
compile time is important for things like passing it as an argument to a
276+
function, moving it about on the stack and allocating (and deallocating) space
277+
on the heap to store it.
278+
279+
For `Foo`, we would need to have a value that could be at least either a
280+
`String` (24 bytes) or a `u8` (1 byte), as well as any other type for which
281+
dependent crates may implement `Foo` (any number of bytes at all). There's no
282+
way to guarantee that this last point can work if the values are stored without
283+
a pointer, because those other types can be arbitrarily large.
284+
285+
Putting the value behind a pointer means the size of the value is not relevant
286+
when we are tossing a trait object around, only the size of the pointer itself.

src/doc/trpl/traits.md

+2-45
Original file line numberDiff line numberDiff line change
@@ -270,51 +270,8 @@ not, because both the trait and the type aren't in our crate.
270270

271271
One last thing about traits: generic functions with a trait bound use
272272
*monomorphization* (*mono*: one, *morph*: form), so they are statically
273-
dispatched. What's that mean? Well, let's take a look at `print_area` again:
274-
275-
```{rust,ignore}
276-
fn print_area<T: HasArea>(shape: T) {
277-
println!("This shape has an area of {}", shape.area());
278-
}
279-
280-
fn main() {
281-
let c = Circle { ... };
282-
283-
let s = Square { ... };
284-
285-
print_area(c);
286-
print_area(s);
287-
}
288-
```
289-
290-
When we use this trait with `Circle` and `Square`, Rust ends up generating
291-
two different functions with the concrete type, and replacing the call sites with
292-
calls to the concrete implementations. In other words, you get something like
293-
this:
294-
295-
```{rust,ignore}
296-
fn __print_area_circle(shape: Circle) {
297-
println!("This shape has an area of {}", shape.area());
298-
}
299-
300-
fn __print_area_square(shape: Square) {
301-
println!("This shape has an area of {}", shape.area());
302-
}
303-
304-
fn main() {
305-
let c = Circle { ... };
306-
307-
let s = Square { ... };
308-
309-
__print_area_circle(c);
310-
__print_area_square(s);
311-
}
312-
```
313-
314-
The names don't actually change to this, it's just for illustration. But
315-
as you can see, there's no overhead of deciding which version to call here,
316-
hence *statically dispatched*. The downside is that we have two copies of
317-
the same function, so our binary is a little bit larger.
273+
dispatched. What's that mean? Check out the chapter on [static and dynamic
274+
dispatch](static-and-dynamic-dispatch.html) for more.
318275

319276
## Our `inverse` Example
320277

0 commit comments

Comments
 (0)