Item 10: Familiarize yourself with standard traits
Rust encodes key behavioral aspects of its type system in the type system itself, through a collection of fine-grained standard traits that describe those behaviors (see Item 2).
Many of these traits will seem familiar to programmers coming from C++, corresponding to concepts such as copy-constructors, destructors, equality and assignment operators, etc.
As in C++, it's often a good idea to implement many of these traits for your own types; the Rust compiler will give you helpful error messages if some operation needs one of these traits for your type and it isn't present.
Implementing such a large collection of traits may seem daunting, but most of the common ones can be automatically
applied to user-defined types, using derive
macros. These derive
macros generate code
with the "obvious" implementation of the trait for that type (e.g., field-by-field comparison for Eq
on a struct
);
this normally requires that all constituent parts also implement the trait. The auto-generated implementation is
usually what you want, but there are occasional exceptions discussed in each trait's section that follows.
The use of the derive
macros does lead to type definitions like:
#![allow(unused)] fn main() { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] enum MyBooleanOption { Off, On, } }
where auto-generated implementations are triggered for eight different traits.
This fine-grained specification of behavior can be disconcerting at first, but it's important to be familiar with the most common of these standard traits so that the available behaviors of a type definition can be immediately understood.
Common Standard Traits
This section discusses the most commonly encountered standard traits. Here are rough one-sentence summaries of each:
Clone
: Items of this type can make a copy of themselves when asked, by running user-defined code.Copy
: If the compiler makes a bit-for-bit copy of this item's memory representation (without running any user-defined code), the result is a valid new item.Default
: It's possible to make new instances of this type with sensible default values.PartialEq
: There's a partial equivalence relation for items of this type—any two items can be definitively compared, but it may not always be true thatx==x
.Eq
: There's an equivalence relation for items of this type—any two items can be definitively compared, and it is always true thatx==x
.PartialOrd
: Some items of this type can be compared and ordered.Ord
: All items of this type can be compared and ordered.Hash
: Items of this type can produce a stable hash of their contents when asked.Debug
: Items of this type can be displayed to programmers.Display
: Items of this type can be displayed to users.
These traits can all be derive
d for user-defined types, with the exception of Display
(included here because of its
overlap with Debug
). However, there are occasions when a manual implementation—or no implementation—is
preferable.
The following sections discuss each of these common traits in more detail.
Clone
The Clone
trait indicates that it's possible to make a new copy of an item, by calling the
clone()
method. This is roughly equivalent to
C++'s copy-constructor but is more explicit: the compiler will never silently invoke this method on its own (read on to
the next section for that).
Clone
can be derive
d for a type if all of the item's fields implement Clone
themselves. The derive
d
implementation clones an aggregate type by cloning each of its members in turn; again, this is roughly equivalent to a
default copy-constructor in C++. This makes the trait opt-in (by adding #[derive(Clone)]
), in contrast to the opt-out
behavior in C++ (MyType(const MyType&) = delete;
).
This is such a common and useful operation that it's more interesting to investigate the situations where you shouldn't
or can't implement Clone
, or where the default derive
implementation isn't appropriate.
- You shouldn't implement
Clone
if the item embodies unique access to some resource (such as an RAII type; Item 11), or when there's another reason to restrict copies (e.g., if the item holds cryptographic key material). - You can't implement
Clone
if some component of your type is un-Clone
able in turn. Examples include the following:- Fields that are mutable references (
&mut T
), because the borrow checker (Item 15) allows only a single mutable reference at a time. - Standard library types that fall into the previous category, such as
MutexGuard
(embodies unique access) orMutex
(restricts copies for thread safety).
- Fields that are mutable references (
- You should manually implement
Clone
if there is anything about your item that won't be captured by a (recursive) field-by-field copy or if there is additional bookkeeping associated with item lifetimes. For example, consider a type that tracks the number of extant items at runtime for metrics purposes; a manualClone
implementation can ensure the counter is kept accurate.
Copy
The Copy
trait has a trivial declaration:
#![allow(unused)] fn main() { pub trait Copy: Clone { } }
There are no methods in this trait, meaning that it is a marker trait (as described in Item 2): it's used to indicate some constraint on the type that's not directly expressed in the type system.
In the case of Copy
, the meaning of this marker is that a bit-for-bit copy of the memory holding an item gives a
correct new item. Effectively, this trait is a marker that says that a type is a "plain old data" (POD)
type.
This also means that the Clone
trait bound can be slightly confusing: although a Copy
type has to implement Clone
,
when an instance of the type is copied, the clone()
method is not invoked—the compiler builds the new item
without any involvement of user-defined code.
In contrast to user-defined marker traits (Item 2), Copy
has a special significance to the compiler (as do several
of the other marker traits in std::marker
) over and above being available for trait bounds—it shifts the
compiler from move semantics to copy semantics.
With move semantics for the assignment operator, what the right hand giveth, the left hand taketh away:
#[derive(Debug, Clone)]
struct KeyId(u32);
let k = KeyId(42);
let k2 = k; // value moves out of k into k2
println!("k = {k:?}");
error[E0382]: borrow of moved value: `k`
--> src/main.rs:60:23
|
58 | let k = KeyId(42);
| - move occurs because `k` has type `main::KeyId`, which does
| not implement the `Copy` trait
59 | let k2 = k; // value moves out of k into k2
| - value moved here
60 | println!("k = {k:?}");
| ^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl`
help: consider cloning the value if the performance cost is acceptable
|
59 | let k2 = k.clone(); // value moves out of k into k2
| ++++++++
With copy semantics, the original item lives on:
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy)] struct KeyId(u32); let k = KeyId(42); let k2 = k; // value bitwise copied from k to k2 println!("k = {k:?}"); }
This makes Copy
one of the most important traits to watch out for: it fundamentally changes the behavior of
assignments—including parameters for method invocations.
In this respect, there are again overlaps with C++'s copy-constructors, but it's worth emphasizing a key distinction: in
Rust there is no way to get the compiler to silently invoke user-defined code—it's either explicit (a call to
.clone()
) or it's not user-defined (a bitwise copy).
Because Copy
has a Clone
trait bound, it's possible to .clone()
any Copy
-able item. However, it's not a good
idea: a bitwise copy will always be faster than invoking a trait method. Clippy (Item 29) will warn you about
this:
let k3 = k.clone();
warning: using `clone` on type `KeyId` which implements the `Copy` trait
--> src/main.rs:79:14
|
79 | let k3 = k.clone();
| ^^^^^^^^^ help: try removing the `clone` call: `k`
|
As with Clone
, it's worth exploring when you should or should not implement Copy
:
- The obvious: don't implement
Copy
if a bitwise copy doesn't produce a valid item. That's likely to be the case ifClone
needed a manual implementation rather than an automaticallyderive
d implementation. - It may be a bad idea to implement
Copy
if your type is large. The basic promise ofCopy
is that a bitwise copy is valid; however, this often goes hand in hand with an assumption that making the copy is fast. If that's not the case, skippingCopy
prevents accidental slow copies. - You can't implement
Copy
if some component of your type is un-Copy
able in turn. - If all of the components of your type are
Copy
able, then it's usually worth derivingCopy
. The compiler has an off-by-default lintmissing_copy_implementations
that points out opportunities for this.
Default
The Default
trait defines a default constructor, via a
default()
method. This trait can be
derive
d for user-defined types, provided that all of the subtypes involved have a Default
implementation of
their own; if they don't, you'll have to implement the trait manually. Continuing the comparison with C++, notice that a
default constructor has to be explicitly triggered—the compiler does not create one automatically.
The Default
trait can also be derive
d for enum
types, as long as there's a #[default]
attribute to give the
compiler a hint as to which variant is, well, default:
#![allow(unused)] fn main() { #[derive(Default)] enum IceCreamFlavor { Chocolate, Strawberry, #[default] Vanilla, } }
The most useful aspect of the Default
trait is its combination with struct update
syntax. This syntax allows
struct
fields to be initialized by copying or moving their contents from an existing instance of the same struct
,
for any fields that aren't explicitly initialized. The template to copy from is given at the end of the initialization,
after ..
, and the Default
trait provides an ideal template to use:
#![allow(unused)] fn main() { #[derive(Default)] struct Color { red: u8, green: u8, blue: u8, alpha: u8, } let c = Color { red: 128, ..Default::default() }; }
This makes it much easier to initialize structures with lots of fields, only some of which have nondefault values. (The builder pattern, Item 7, may also be appropriate for these situations.)
PartialEq
and Eq
The PartialEq
and Eq
traits allow you to define equality for user-defined types. These traits have
special significance because if they're present, the compiler will automatically use them for equality (==
) checks,
similarly to operator==
in C++. The default derive
implementation does this with a recursive field-by-field
comparison.
The Eq
version is just a marker trait extension of PartialEq
that adds the assumption of
reflexivity: any type T
that claims to support Eq
should ensure that x == x
is true for any x: T
.
This is sufficiently odd to immediately raise the question, When wouldn't x == x
? The primary rationale behind this
split relates to floating point numbers,1 and specifically to the special "not a
number" value NaN (f32::NAN
/ f64::NAN
in Rust). The floating point specifications require
that nothing compares equal to NaN, including NaN itself; the PartialEq
trait is the knock-on effect of this.
For user-defined types that don't have any float-related peculiarities, you should implement Eq
whenever you
implement PartialEq
. The full Eq
trait is also required if you want to use the type as the key in a
HashMap
(as well as the Hash
trait).
You should implement PartialEq
manually if your type contains any fields that do not affect the item's identity,
such as internal caches and other performance optimizations. (Any manual implementation will also be used for Eq
if
it is defined, because Eq
is just a marker trait that has no methods of its own.)
PartialOrd
and Ord
The ordering traits PartialOrd
and Ord
allow comparisons between two items of a type, returning Less
,
Greater
, or Equal
. The traits require equivalent equality traits to be implemented (PartialOrd
requires
PartialEq
; Ord
requires Eq
), and the two have to agree with each other (watch out for this with manual
implementations in particular).
As with the equality traits, the comparison traits have special significance because the compiler will automatically use
them for comparison operations (<
, >
, <=
, >=
).
The default implementation produced by derive
compares fields (or enum
variants) lexicographically in the order
they're defined, so if this isn't correct, you'll need to implement the traits manually (or reorder the fields).
Unlike PartialEq
, the PartialOrd
trait does correspond to a variety of real situations. For example, it could be
used to express a subset relationship among collections:2 {1, 2}
is a
subset of {1, 2, 4}
, but {1, 3}
is not a subset of {2, 4}
, nor vice versa.
However, even if a partial order does accurately model the behavior of your type, be wary of implementing just
PartialOrd
and not Ord
(a rare occasion that contradicts the advice in Item 2 to encode behavior in the type
system)—it can lead to surprising results:
#![allow(unused)] fn main() { // Inherit the `PartialOrd` behavior from `f32`. #[derive(PartialOrd, PartialEq)] struct Oddity(f32); // Input data with NaN values is likely to give unexpected results. let x = Oddity(f32::NAN); let y = Oddity(f32::NAN); // A self-comparison looks like it should always be true, but it may not be. if x <= x { println!("This line doesn't get executed!"); } // Programmers are also unlikely to write code that covers all possible // comparison arms; if the types involved implemented `Ord`, then the // second two arms could be combined. if x <= y { println!("y is bigger"); // Not hit. } else if y < x { println!("x is bigger"); // Not hit. } else { println!("Neither is bigger"); } }
Hash
The Hash
trait is used to produce a single value that has a high probability of being different for different
items. This hash value is used as the basis for hash-bucket-based data structures like
HashMap
and
HashSet
; as such, the type of the keys in these
data structures must implement Hash
(and Eq
).
Flipping this around, it's essential that the "same" items (as per Eq
) always produce the same hash: if x == y
(via
Eq
), then it must always be true that hash(x) == hash(y)
. If you have a manual Eq
implementation, check
whether you also need a manual implementation of Hash
to comply with this requirement.
Debug
and Display
The Debug
and Display
traits allow a type to specify how it should be included in output, for either
normal ({}
format argument) or debugging purposes ({:?}
format argument), roughly analogous to an operator<<
overload for iostream
in C++.
The differences between the intents of the two traits go beyond which format specifier is needed, though:
Debug
can be automatically derived,Display
can only be manually implemented.- The layout of
Debug
output may change between different Rust versions. If the output will ever be parsed by other code, useDisplay
. Debug
is programmer-oriented;Display
is user-oriented. A thought experiment that helps with this is to consider what would happen if the program was localized to a language that the authors don't speak—Display
is appropriate if the content should be translated,Debug
if not.
As a general rule, add an automatically generated Debug
implementation for your types unless they contain
sensitive information (personal details, cryptographic material, etc.). To make this advice easier to comply with, the
Rust compiler includes a
missing_debug_implementations
lint that points out types without Debug
. This lint is disabled by default but can be enabled for your code with either of the following:
#![allow(unused)] #![warn(missing_debug_implementations)] fn main() { }
#![allow(unused)] #![deny(missing_debug_implementations)] fn main() { }
If the automatically generated implementation of Debug
would emit voluminous amounts of detail, then it may be more
appropriate to include a manual implementation of Debug
that summarizes the type's contents.
Implement Display
if your types are designed to be shown to end users in textual output.
Standard Traits Covered Elsewhere
In addition to the common traits described in the previous section, the standard library also includes other standard traits that are less ubiquitous. Of these additional standard traits, the following are the most important, but they are covered in other Items and so are not covered here in depth:
Fn
,FnOnce
, andFnMut
: Items implementing these traits represent closures that can be invoked. See Item 2.Error
: Items implementing this trait represent error information that can be displayed to users or programmers, and that may hold nested suberror information. See Item 4.Drop
: Items implementing this trait perform processing when they are destroyed, which is essential for RAII patterns. See Item 11.From
andTryFrom
: Items implementing these traits can be automatically created from items of some other type but with a possibility of failure in the latter case. See Item 5.Deref
andDerefMut
: Items implementing these traits are pointer-like objects that can be dereferenced to get access to an inner item. See Item 8.Iterator
and friends: Items implementing these traits represent collections that can be iterated over. See Item 9.Send
: Items implementing this trait are safe to transfer between multiple threads. See Item 17.Sync
: Items implementing this trait are safe to be referenced by multiple threads. See Item 17.
None of these traits are derive
able.
Operator Overloads
The final category of standard traits relates to operator overloads, where Rust allows various built-in unary and binary
operators to be overloaded for user-defined types, by implementing various standard traits from the std::ops
module. These traits are not derivable and are typically needed only
for types that represent "algebraic" objects, where there is a natural interpretation of these operators.
However, experience from C++ has shown that it's best to avoid overloading operators for unrelated types as
it often leads to code that is hard to maintain and has unexpected performance properties (e.g., x + y
silently invokes
an expensive O(N) method).
To comply with the principle of least astonishment, if you implement any operator overloads, you should implement a coherent set of operator overloads. For example, if x + y
has an overload
(Add
), and -y
(Neg
) does too, then you should also implement x - y
(Sub
) and make sure it gives the same answer as x + (-y)
.
The items passed to the operator overload traits are moved, which means that non-Copy
types will be consumed by
default. Adding implementations for &'a MyType
can help with this but requires more boilerplate to cover all of the
possibilities (e.g., there are 4 = 2 × 2 possibilities for combining reference/non-reference arguments to a binary
operator).
Summary
This item has covered a lot of ground, so some tables that summarize the standard traits that have been touched on are
in order. First, Table 2-1 covers the traits that this Item covers in depth, all of which can be automatically
derive
d except Display
.
Trait | Compiler Use | Bound | Methods |
---|---|---|---|
Clone | clone | ||
Copy | let y = x; | Clone | Marker trait |
Default | default | ||
PartialEq | x == y | eq | |
Eq | x == y | PartialEq | Marker trait |
PartialOrd | x < y , x <= y , … | PartialEq | partial_cmp |
Ord | x < y , x <= y , … | Eq + PartialOrd | cmp |
Hash | hash | ||
Debug | format!("{:?}", x) | fmt | |
Display | format!("{}", x) | fmt |
The operator overloads are summarized in Table 2-2.3
None of these can be derive
d.
Trait | Compiler Use | Bound | Methods |
---|---|---|---|
Add | x + y | add | |
AddAssign | x += y | add_assign | |
BitAnd | x & y | bitand | |
BitAndAssign | x &= y | bitand_assign | |
BitOr | x ⎮ y | bitor | |
BitOrAssign | x ⎮= y | bitor_assign | |
BitXor | x ^ y | bitxor | |
BitXorAssign | x ^= y | bitxor_assign | |
Div | x / y | div | |
DivAssign | x /= y | div_assign | |
Mul | x * y | mul | |
MulAssign | x *= y | mul_assign | |
Neg | -x | neg | |
Not | !x | not | |
Rem | x % y | rem | |
RemAssign | x %= y | rem_assign | |
Shl | x << y | shl | |
ShlAssign | x <<= y | shl_assign | |
Shr | x >> y | shr | |
ShrAssign | x >>= y | shr_assign | |
Sub | x - y | sub | |
SubAssign | x -= y | sub_assign |
For completeness, the standard traits that are covered in other items are included in Table 2-3; none of these
traits are derive
able (but Send
and Sync
may be automatically implemented by the compiler).
Trait | Item | Compiler Use | Bound | Methods |
---|---|---|---|---|
Fn | Item 2 | x(a) | FnMut | call |
FnMut | Item 2 | x(a) | FnOnce | call_mut |
FnOnce | Item 2 | x(a) | call_once | |
Error | Item 4 | Display + Debug | [source ] | |
From | Item 5 | from | ||
TryFrom | Item 5 | try_from | ||
Into | Item 5 | into | ||
TryInto | Item 5 | try_into | ||
AsRef | Item 8 | as_ref | ||
AsMut | Item 8 | as_mut | ||
Borrow | Item 8 | borrow | ||
BorrowMut | Item 8 | Borrow | borrow_mut | |
ToOwned | Item 8 | to_owned | ||
Deref | Item 8 | *x , &x | deref | |
DerefMut | Item 8 | *x , &mut x | Deref | deref_mut |
Index | Item 8 | x[idx] | index | |
IndexMut | Item 8 | x[idx] = ... | Index | index_mut |
Pointer | Item 8 | format("{:p}", x) | fmt | |
Iterator | Item 9 | next | ||
IntoIterator | Item 9 | for y in x | into_iter | |
FromIterator | Item 9 | from_iter | ||
ExactSizeIterator | Item 9 | Iterator | (size_hint ) | |
DoubleEndedIterator | Item 9 | Iterator | next_back | |
Drop | Item 11 | } (end of scope) | drop | |
Sized | Item 12 | Marker trait | ||
Send | Item 17 | cross-thread transfer | Marker trait | |
Sync | Item 17 | cross-thread use | Marker trait |
Of course, comparing floats for equality is always a dangerous game, as there is typically no guarantee that rounded calculations will produce a result that is bit-for-bit identical to the number you first thought of.
More generally, any lattice structure also has a partial order.
Some of the names here are a little cryptic—e.g., Rem
for remainder and Shl
for shift left—but the
std::ops
documentation makes the intended use clear.