Item 5: Familiarize yourself with standard traits

Rust encodes key behavioural aspects of its type system in the type system itself, through a collection of fine-grained standard traits that describe those behaviours.

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 usually 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, through use of derive macros. This leads to type definitions like:

    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    enum MyBooleanOption {
        Off,
        On,
    }

This fine-grained specification of behaviour can be disconcerting at first, but it's important to be familiar with the most common of these standard crates so that the available behaviours of a type definition can be immediately understood.

A rough one-sentence summary each of the standard traits that this Item covers is:

  • Clone: Items of this type can make a copy of themselves when asked.
  • Copy: If the compiler makes a bit-for-bit copy of this item's memory representation, the result is a valid new item.
  • Default: It's possible to make new instance 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's not always true that x==x.
  • Eq. There's an equivalence relation for items of this type: any two items can be definitively compared.
  • 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 derived 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.

Rust also allows various built-in unary and binary operators to be overloaded for user-defined types, by implementing various traits from the std::ops module. These traits are not derivable, and are typically only needed for types that represent "algebraic" objects.

Other (non-deriveable) standard traits are covered in other Items, and so are not included here. These include:

  • Fn, FnOnce and FnMut: Items of this type represent closures that can be invoked. See Item 2.
  • Error: Items of this type represent error information that can be displayed to users or programmers, and which may hold nested sub-error information. See Item 4.
  • Drop: Items of this type perform processing when they are destroyed, which is essential for RAII patterns. See Item 10.
  • From and TryFrom: Items of this type can be automatically created from items of some other type, but with a possibility of failure in the latter case. See Item 6.
  • Deref and DerefMut: Items of this type are pointer-like objects that can be dereferenced to get access to an inner item. See Item 8.
  • Iterator and friends: Items of this type can be iterated over. See Item 9.
  • Send and Sync: Items of this type are safe to transfer between, or be referenced by, multiple threads. See Item 16.

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 more explicit: the compiler will never silently invoke this method on its own (read on to the next section for that).

Clone can be derived; the macro implementation clones an aggregate type by cloning each of its members in turn, again, 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 behaviour 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 10), 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-Cloneable in turn. Examples include:
    • Fields that are mutable references (&mut T), because the borrow checker (Item 13) only allows a single mutable reference at a time.
    • Standard library types that fall in to the previous category, such as Mutex or MutexGuard.
  • 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 book-keeping associated with item lifetimes (for example: consider a type that tracks the number of extant items at runtime for metrics purposes)

Copy

The Copy trait has a trivial declaration:

pub trait Copy: Clone { }

There are no methods in this trait, meaning that it is a marker trait – it's used to indicate some constraint on a type that's not directly expressed in the type system.

In the case of Copy, the meaning of this marker is that not only can items of this type be copied (hence the Clone trait bound), but also 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.

In contrast to user-defined marker traits (Item 1), Copy has a special significance to the compiler1 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)]
        struct KeyId(u32);
        let k = KeyId(42);
        let k2 = k; // value moves out of k in to k2
        println!("k={:?}", k);
error[E0382]: borrow of moved value: `k`
  --> std-traits/src/main.rs:52:28
   |
50 |         let k = KeyId(42);
   |             - move occurs because `k` has type `main::KeyId`, which does not implement the `Copy` trait
51 |         let k2 = k; // value moves out of k in to k2
   |                  - value moved here
52 |         println!("k={:?}", k);
   |                            ^ value borrowed here after move

With copy semantics, the original item lives on:

        #[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 behaviour of assignments – and this includes 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).

To finish this section, observe that 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 28) will warn you about this:

        let k3 = k.clone();
warning: using `clone` on type `main::KeyId` which implements the `Copy` trait
  --> std-traits/src/main.rs:68:18
   |
68 |         let k3 = k.clone();
   |                  ^^^^^^^^^ help: try removing the `clone` call: `k`
   |
   = note: `#[warn(clippy::clone_on_copy)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy

Default

The Default trait defines a default constructor, via a default()) method. This trait can be derived for user-defined types, provided that all of the sub-types 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 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:

    #[derive(Default)]
    struct Colour {
        red: u8,
        green: u8,
        blue: u8,
        alpha: u8,
    }

    let c = Colour {
        red: 128,
        ..Default::default()
    };

This makes it much easier to initialize structures with lots of fields, only some of which have non-default 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 which 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 numbers2, 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.

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 re-order 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 relationship3 among collections: {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 behaviour of your type, be wary of implementing just PartialOrd (a rare occasion that contradicts the advice of Item 1) – it can lead to surprising results:

    let x = Oddity(1);
    if x <= x {
        println!("Never hit this!");
    }

    let y = Oddity(2);
    if x <= y {
        println!("y is bigger"); // Not hit
    } else if y <= x {
        // Programmers are likely to omit this arm
        println!("x is bigger"); // Not hit
    } else {
        println!("neither is bigger"); // This one
    }

Hash

The Hash trait is used to produce a single value that has a high probability of being different for different items; this value is used as the basis for hash-bucket based data structures like HashMap and HashSet.

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 iostream operator<< overload 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. This is related to…
  • The layout of Debug output may change between different Rust versions. If the output will ever be parsed by other code, use Display.
  • 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.). A manual implementation of Debug can be appropriate when the automatically generated version would emit voluminous amounts of detail.

Implement Display if your types are designed to be shown to end users in textual output.

Operator Overloads

Similarly to C++, Rust allows various arithmetic and bitwise operators to be overloaded for user-defined types. This useful for "algebraic" or bit-manipulation types (respectively) 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).

Continuing with the principle of least surprise, 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), 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. 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, the traits of this Item, all of which can be automatically derived except Display.

TraitCompiler UseBoundMethods
Cloneclone
Copylet y = x;CloneMarker trait
Defaultdefault
PartialEqx == yeq
Eqx == yPartialEqMarker trait
PartialOrdx < y,
x <= y,
PartialEqpartial_cmp
Ordx < y,
x <= y,
Eq + PartialOrdcmp
Hashhash
Debugformat!("{:?}", x)fmt
Displayformat!("{}", x)fmt

The operator overloads are in the next table. None of these can be derived.

TraitCompiler UseBoundMethods
Addx + yadd
AddAssignx += yadd_assign
BitAndx & ybitand
BitAndAssignx &= ybitand_assign
BitOrx ⎮ ybitor
BitOrAssignx ⎮= ybitor_assign
BitXorx ^ ybitxor
BitXorAssignx ^= ybitxor_assign
Divx / ydiv
DivAssignx /= ydiv_assign
Indexx[y]index
IndexMutx[y]index_mut
Mulx * ymul
MulAssignx *= ymul_assign
Neg-xneg
Not!xnot
Remx % yrem
RemAssignx %= yrem_assign
Shlx << yshl
ShlAssignx <<= yshl_assign
Shrx >> yshr
ShrAssignx >>= yshr_assign
Subx - ysub
SubAssignx -= ysub_assign

For completeness, the standard traits that are covered in other items are included in the following table; none of these traits are deriveable (but Send and Sync may be automatically implemented by the compiler).

TraitItemCompiler UseBoundMethods
FnItem 2x(a)FnMutcall
FnOnceItem 2x(a)FnOncecall_mut
FnMutItem 2x(a)call_once
ErrorItem 4Display + Debug[source]
FromItem 6from
TryFromItem 6try_from
IntoItem 6into
TryIntoItem 6try_into
AsRefItem 8as_ref
AsMutItem 8as_mut
BorrowItem 8borrow
BorrowMutItem 8Borrowborrow_mut
ToOwnedItem 8 to_owned
DerefItem 8*x, &xderef
DerefMutItem 8*x, &mut xDerefderef_mut
IndexItem 8x[idx]index
IndexMutItem 8x[idx] = ...Indexindex_mut
PointerItem 8format("{:p}", x)fmt
IteratorItem 9next
IntoIteratorItem 9for y in xinto_iter
FromIteratorItem 9from_iter
ExactSizeIteratorItem 9Iterator(size_hint)
DoubleEndedIteratorItem 9Iteratornext_back
DropItem 10} (end of scope)drop
SizedItem 13Marker trait
SendItem 16cross-thread transferMarker trait
SyncItem 16cross-thread useMarker trait


1: As do several of the other marker traits in std::marker.

2: 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.

3: More generally, any lattice structure also has a partial order.