Skip to content
Learn Rust
Theme

Common Traits

Abstractions · Lesson 34

Almost every type you write needs the same few abilities: print, copy, compare, use as a key, start from a default, convert. Each one is a standard trait. #[derive(...)] writes the obvious ones from the fields, and you write the rest by hand.

Coming from GoGo gives a struct == (when every field is comparable), map-key use and %v printing without being asked. Rust makes each one a trait you ask for, usually in one derive line. String() is impl Display, and cmp.Compare is Ord::cmp (floats use total_cmp).

Coming from KotlinA data class writes equals, hashCode, toString and copy. In Rust you list the traits you want: #[derive(Debug, Clone, PartialEq, Eq, Hash)]. Comparable<T> is Ord, and a hand-written toString() is impl Display.

Coming from Python__eq__, __hash__, __repr__, __str__ and __lt__ become PartialEq/Eq, Hash, Debug, Display and PartialOrd/Ord. @dataclass(order=True, frozen=True) is the closest thing to one derive line. A bare Rust struct has none of them.

Coming from TypeScriptA class compares by identity and has no ordering, and a Map keys objects by identity too. In Rust a derive line gives equality, ordering and hashing by value, and toString() is impl Display.

Coming from Javaequals, hashCode, toString and compareTo become PartialEq/Eq, Hash, Debug/Display and Ord. A record writes the first three for you; in Rust the same job is #[derive(PartialEq, Eq, Hash, Debug)], so equals and hashCode cannot disagree.

Coming from C#Equals, GetHashCode, ToString and IComparable<T> become PartialEq/Eq, Hash, Debug/Display and Ord. A record writes the first three; in Rust you list them in a derive line.

Coming from CC has no traits. A struct has no ==, cannot be printed and has no hash, so each one is a function you write and call by name (qsort takes a comparison function). A Rust trait is that function, named once and found by the compiler.

Coming from C++= default on operator== and operator<=> is the nearest thing to a derive. A copy constructor is Clone, but it runs only at .clone(): b = a moves. A destructor is Drop, and a std::hash specialisation is Hash. In Rust all of them are traits with fixed names.

The quick comparison

Rustsrc/main.rsRun ↗
use std::collections::HashSet;
use std::fmt;
// Each name in `derive` asks the compiler to write one standard ability for us.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Version {
major: u32,
minor: u32,
}
// Display (the `{}` text for users) cannot be derived: we choose how it looks.
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "v{}.{}", self.major, self.minor)
}
}
fn main() {
let a = Version {
major: 1,
minor: 10,
};
let b = Version { major: 1, minor: 9 };
println!("display: {a}");
println!("a == b: {}", a == b);
println!("a > b: {}", a > b);
let mut all = vec![a, b, Version::default()];
all.sort();
let names: Vec<String> = all.iter().map(|v| v.to_string()).collect();
println!("sorted: {}", names.join(" "));
let mut seen = HashSet::new();
for v in [a, b, Version::default(), a] {
seen.insert(v);
}
println!("distinct: {}", seen.len());
}
$ cargo run
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
package main
import (
"cmp"
"fmt"
"slices"
"strings"
)
// A struct of comparable fields gets == and works as a map key with nothing declared.
type Version struct {
Major, Minor uint32
}
// Stringer is Go's Display: fmt calls String() for %v and %s.
func (v Version) String() string {
return fmt.Sprintf("v%d.%d", v.Major, v.Minor)
}
// Ordering is not automatic: write the comparison.
func compare(a, b Version) int {
if c := cmp.Compare(a.Major, b.Major); c != 0 {
return c
}
return cmp.Compare(a.Minor, b.Minor)
}
func main() {
a := Version{1, 10}
b := Version{1, 9}
fmt.Println("display:", a)
fmt.Println("a == b:", a == b)
fmt.Println("a > b:", compare(a, b) > 0)
all := []Version{a, b, {}}
slices.SortFunc(all, compare)
names := []string{}
for _, v := range all {
names = append(names, v.String())
}
fmt.Println("sorted:", strings.Join(names, " "))
seen := map[Version]bool{}
for _, v := range []Version{a, b, {}, a} {
seen[v] = true
}
fmt.Println("distinct:", len(seen))
}
$ go run main.go
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
// A data class writes equals, hashCode and toString from the constructor properties.
data class Version(val major: Int, val minor: Int) : Comparable<Version> {
// Ordering is not automatic: Comparable is the one to implement.
override fun compareTo(other: Version): Int =
compareValuesBy(this, other, Version::major, Version::minor)
// toString is the user-facing text (a data class's own would be "Version(major=1, minor=10)").
override fun toString() = "v$major.$minor"
}
fun main() {
val a = Version(1, 10)
val b = Version(1, 9)
println("display: $a")
println("a == b: ${a == b}")
println("a > b: ${a > b}")
val all = listOf(a, b, Version(0, 0)).sorted()
println("sorted: ${all.joinToString(" ")}")
val seen = hashSetOf<Version>()
for (v in listOf(a, b, Version(0, 0), a)) seen.add(v)
println("distinct: ${seen.size}")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
from dataclasses import dataclass
# order=True writes <, > from the fields in order; frozen=True makes it hashable.
@dataclass(order=True, frozen=True)
class Version:
major: int = 0
minor: int = 0
# __str__ is the user-facing text (the dataclass's __repr__ is the debug text).
def __str__(self):
return f"v{self.major}.{self.minor}"
a = Version(1, 10)
b = Version(1, 9)
print("display:", a)
print("a == b:", str(a == b).lower())
print("a > b:", str(a > b).lower())
all_versions = sorted([a, b, Version()])
print("sorted:", " ".join(str(v) for v in all_versions))
seen = set()
for v in [a, b, Version(), a]:
seen.add(v)
print("distinct:", len(seen))
$ python3 main.py
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
// A class has no == by value, no ordering and no usable Map key: write each by hand.
class Version {
constructor(
readonly major: number,
readonly minor: number,
) {}
// toString is the user-facing text.
toString(): string {
return `v${this.major}.${this.minor}`;
}
equals(other: Version): boolean {
return this.major === other.major && this.minor === other.minor;
}
compareTo(other: Version): number {
return this.major - other.major || this.minor - other.minor;
}
// A Map compares object keys by identity, so key on a string built from the fields.
key(): string {
return `${this.major}.${this.minor}`;
}
}
const a = new Version(1, 10);
const b = new Version(1, 9);
console.log("display:", String(a));
console.log("a == b:", a.equals(b));
console.log("a > b:", a.compareTo(b) > 0);
const all = [a, b, new Version(0, 0)].sort((x, y) => x.compareTo(y));
console.log("sorted:", all.map(String).join(" "));
const seen = new Set<string>();
for (const v of [a, b, new Version(0, 0), a]) {
seen.add(v.key());
}
console.log("distinct:", seen.size);
$ tsc --strict --module nodenext --types node main.ts && node main.js
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
public class Main {
// A record writes equals, hashCode and toString from its components.
// Ordering is not automatic: Comparable is the one to implement.
record Version(int major, int minor) implements Comparable<Version> {
@Override
public int compareTo(Version other) {
return major != other.major ? Integer.compare(major, other.major) : Integer.compare(minor, other.minor);
}
// toString is the user-facing text (a record's own would be "Version[major=1, minor=10]").
@Override
public String toString() {
return "v" + major + "." + minor;
}
}
public static void main(String[] args) {
Version a = new Version(1, 10);
Version b = new Version(1, 9);
System.out.println("display: " + a);
System.out.println("a == b: " + a.equals(b));
System.out.println("a > b: " + (a.compareTo(b) > 0));
List<Version> all = new ArrayList<>(List.of(a, b, new Version(0, 0)));
all.sort(null);
List<String> names = new ArrayList<>();
for (Version v : all) names.add(v.toString());
System.out.println("sorted: " + String.join(" ", names));
HashSet<Version> seen = new HashSet<>();
for (Version v : List.of(a, b, new Version(0, 0), a)) seen.add(v);
System.out.println("distinct: " + seen.size());
}
}
$ javac Main.java && java Main
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
// A record struct writes Equals, GetHashCode and ToString from its properties.
// Ordering is not automatic: IComparable is the one to implement.
var a = new Version(1, 10);
var b = new Version(1, 9);
Console.WriteLine($"display: {a}");
Console.WriteLine($"a == b: {(a == b).ToString().ToLower()}");
Console.WriteLine($"a > b: {(a.CompareTo(b) > 0).ToString().ToLower()}");
var all = new List<Version> { a, b, new Version(0, 0) };
all.Sort();
Console.WriteLine($"sorted: {string.Join(" ", all)}");
var seen = new HashSet<Version>();
foreach (var v in new[] { a, b, new Version(0, 0), a }) seen.Add(v);
Console.WriteLine($"distinct: {seen.Count}");
readonly record struct Version(int Major, int Minor) : IComparable<Version>
{
public int CompareTo(Version other) =>
Major != other.Major ? Major.CompareTo(other.Major) : Minor.CompareTo(other.Minor);
// ToString is the user-facing text (a record's own would be "Version { Major = 1, Minor = 10 }").
public override string ToString() => $"v{Major}.{Minor}";
}
$ dotnet run
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
#include <stdio.h>
#include <stdlib.h>
// C has no traits: no ==, no printing and no ordering for a struct.
// Each one is a function you write and call by name.
typedef struct {
unsigned major;
unsigned minor;
} Version;
int version_cmp(const void *pa, const void *pb) {
const Version *a = pa, *b = pb;
if (a->major != b->major) return a->major < b->major ? -1 : 1;
if (a->minor != b->minor) return a->minor < b->minor ? -1 : 1;
return 0;
}
int main(void) {
Version a = {1, 10};
Version b = {1, 9};
printf("display: v%u.%u\n", a.major, a.minor);
printf("a == b: %s\n", version_cmp(&a, &b) == 0 ? "true" : "false");
printf("a > b: %s\n", version_cmp(&a, &b) > 0 ? "true" : "false");
Version all[3] = {a, b, {0, 0}};
qsort(all, 3, sizeof(Version), version_cmp);
printf("sorted:");
for (int i = 0; i < 3; i++) printf(" v%u.%u", all[i].major, all[i].minor);
printf("\n");
// No hash set in the standard library: count distinct values by comparing.
Version list[4] = {a, b, {0, 0}, a};
int distinct = 0;
for (int i = 0; i < 4; i++) {
int dup = 0;
for (int j = 0; j < i; j++) {
if (version_cmp(&list[i], &list[j]) == 0) dup = 1;
}
if (!dup) distinct++;
}
printf("distinct: %d\n", distinct);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3
#include <algorithm>
#include <compare>
#include <iostream>
#include <set>
#include <string>
#include <vector>
// A defaulted <=> writes <, > and the rest from the members in order; a defaulted ==
// writes == (and != follows from it).
struct Version {
unsigned major = 0;
unsigned minor = 0;
auto operator<=>(const Version &) const = default;
bool operator==(const Version &) const = default;
};
// operator<< is the user-facing text; nothing prints a struct otherwise.
std::ostream &operator<<(std::ostream &os, const Version &v) {
return os << "v" << v.major << "." << v.minor;
}
int main() {
Version a{1, 10};
Version b{1, 9};
std::cout << "display: " << a << "\n";
std::cout << "a == b: " << std::boolalpha << (a == b) << "\n";
std::cout << "a > b: " << (a > b) << "\n";
std::vector<Version> all{a, b, Version{}};
std::sort(all.begin(), all.end());
std::cout << "sorted:";
for (const Version &v : all) std::cout << " " << v;
std::cout << "\n";
// std::set orders with operator<; std::unordered_set would also need a std::hash.
std::set<Version> seen;
for (const Version &v : {a, b, Version{}, a}) seen.insert(v);
std::cout << "distinct: " << seen.size() << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
display: v1.10
a == b: false
a > b: true
sorted: v0.0 v1.9 v1.10
distinct: 3

What's different

  • derive writes the obvious code from the fields. Display cannot be derived, because there is no obvious text: you write impl Display, and to_string() comes with it.
  • Debug ({:?}) is for developers and Display ({}) is for users. A type has neither until you add it.
  • Some traits need others. Eq needs PartialEq, Ord needs PartialOrd and Eq, and Copy needs Clone.
  • f64 has PartialEq and PartialOrd but not Eq, Ord or Hash, because NaN is not equal to itself. A struct with an f64 field cannot derive them either.
Gotcha

Using your own type as a map key

Looks fine: in Go, Kotlin, Python, Java and C# it runs fine; in TypeScript it runs but gives a wrong answer. Rust won't compile it.

Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Broken in TypeScript. Rust catches it.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.No direct equivalent in C. Just a Rust rule.C++ refuses this too. Same idea, different words.
Rustsrc/main.rs✗ won't compile
use std::collections::HashMap;
struct Version {
major: u32,
minor: u32,
}
fn main() {
let mut notes = HashMap::new();
notes.insert(Version { major: 1, minor: 2 }, "stable");
let found = notes.get(&Version { major: 1, minor: 2 });
println!("found: {found:?}");
}
package main
import "fmt"
type Version struct {
Major, Minor uint32
}
func main() {
notes := map[Version]string{}
notes[Version{1, 2}] = "stable"
found, ok := notes[Version{1, 2}]
fmt.Println("found:", found, ok)
}
$ go run main.go✓ runs
found: stable true
data class Version(val major: Int, val minor: Int)
fun main() {
val notes = hashMapOf<Version, String>()
notes[Version(1, 2)] = "stable"
println("found: ${notes[Version(1, 2)]}")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
found: stable
from dataclasses import dataclass
@dataclass(frozen=True)
class Version:
major: int
minor: int
notes = {}
notes[Version(1, 2)] = "stable"
print("found:", notes.get(Version(1, 2)))
$ python3 main.py✓ runs
found: stable
class Version {
constructor(
readonly major: number,
readonly minor: number,
) {}
}
const notes = new Map<Version, string>();
notes.set(new Version(1, 2), "stable");
console.log("found:", notes.get(new Version(1, 2)));
$ tsc --strict --module nodenext --types node main.ts && node main.js! runs · wrong answer
found: undefined
import java.util.HashMap;
public class Main {
record Version(int major, int minor) {}
public static void main(String[] args) {
HashMap<Version, String> notes = new HashMap<>();
notes.put(new Version(1, 2), "stable");
System.out.println("found: " + notes.get(new Version(1, 2)));
}
}
$ javac Main.java && java Main✓ runs
found: stable
var notes = new Dictionary<Version, string>();
notes[new Version(1, 2)] = "stable";
Console.WriteLine($"found: {notes[new Version(1, 2)]}");
record Version(int Major, int Minor);
$ dotnet run✓ runs
found: stable
Cno equivalent

C has no standard hash map type, so there is nothing to try. A table you write or borrow decides for itself how keys are hashed and compared.

C++✗ refuses too

C++ refuses too. std::unordered_map needs a std::hash<Version>. The error says the unordered_map constructor is a deleted function, and it is long because it is reported from inside the library; look for std::hash<Version> in it. A std::map needs only an operator<.

#include <iostream>
#include <string>
#include <unordered_map>
struct Version {
unsigned major;
unsigned minor;
};
int main() {
std::unordered_map<Version, std::string> notes;
notes[Version{1, 2}] = "stable";
std::cout << "found: " << notes[Version{1, 2}] << "\n";
}
main.cpp: In function ‘int main()’:
main.cpp:11:46: error: use of deleted function ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::unordered_map() [with _Key = Version; _Tp = std::__cxx11::basic_string<char>; _Hash = std::hash<Version>; _Pred = std::equal_to<Version>; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >]’
   11 |     std::unordered_map<Version, std::string> notes;
      |                                              ^~~~~
In file included from /usr/include/c++/14/unordered_map:41,
                 from main.cpp:3:
/usr/include/c++/14/bits/unordered_map.h:148:7: note: ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::unordered_map() [with _Key = Version; _Tp = std::__cxx11::basic_string<char>; _Hash = std::hash<Version>; _Pred = std::equal_to<Version>; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >]’ is implicitly deleted because the default definition would be ill-formed:
  148 |       unordered_map() = default;
      |       ^~~~~~~~~~~~~
/usr/include/c++/14/bits/unordered_map.h: At global scope:
/usr/include/c++/14/bits/unordered_map.h:148:7: error: use of deleted function ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::_Hashtable() [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>]’
In file included from /usr/include/c++/14/bits/unordered_map.h:33:
/usr/include/c++/14/bits/hashtable.h:539:7: note: ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::_Hashtable() [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>]’ is implicitly deleted because the default definition would be ill-formed:
  539 |       _Hashtable() = default;
      |       ^~~~~~~~~~
/usr/include/c++/14/bits/hashtable.h:539:7: error: use of deleted function ‘std::__detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _Traits>::_Hashtable_base() [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _Traits = std::__detail::_Hashtable_traits<true, false, true>]’
In file included from /usr/include/c++/14/bits/hashtable.h:35:
/usr/include/c++/14/bits/hashtable_policy.h:1731:7: note: ‘std::__detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _Traits>::_Hashtable_base() [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _Traits = std::__detail::_Hashtable_traits<true, false, true>]’ is implicitly deleted because the default definition would be ill-formed:
 1731 |       _Hashtable_base() = default;
      |       ^~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1731:7: error: use of deleted function ‘std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Hash, _RangeHash, _Unused, __cache_hash_code>::_Hash_code_base() [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _ExtractKey = std::__detail::_Select1st; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; bool __cache_hash_code = true]’
/usr/include/c++/14/bits/hashtable_policy.h: In instantiation of ‘std::__detail::_Hashtable_ebo_helper<_Nm, _Tp, true>::_Hashtable_ebo_helper() [with int _Nm = 1; _Tp = std::hash<Version>]’:
/usr/include/c++/14/bits/hashtable_policy.h:1328:7:   required from here
 1328 |       _Hash_code_base() = default;
      |       ^~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1245:49: error: use of deleted function ‘std::hash<Version>::hash()’
 1245 |       _Hashtable_ebo_helper() noexcept(noexcept(_Tp())) : _Tp() { }
      |                                                 ^~~~~
In file included from /usr/include/c++/14/string_view:50,
                 from /usr/include/c++/14/bits/basic_string.h:47,
                 from /usr/include/c++/14/string:54,
                 from /usr/include/c++/14/bits/locale_classes.h:40,
                 from /usr/include/c++/14/bits/ios_base.h:41,
                 from /usr/include/c++/14/ios:44,
                 from /usr/include/c++/14/ostream:40,
                 from /usr/include/c++/14/iostream:41,
                 from main.cpp:1:
/usr/include/c++/14/bits/functional_hash.h:102:12: note: ‘std::hash<Version>::hash()’ is implicitly deleted because the default definition would be ill-formed:
  102 |     struct hash : __hash_enum<_Tp>
      |            ^~~~
/usr/include/c++/14/bits/functional_hash.h:102:12: error: no matching function for call to ‘std::__hash_enum<Version, false>::__hash_enum()’
/usr/include/c++/14/bits/functional_hash.h:83:7: note: candidate: ‘std::__hash_enum<_Tp, <anonymous> >::__hash_enum(std::__hash_enum<_Tp, <anonymous> >&&) [with _Tp = Version; bool <anonymous> = false]’
   83 |       __hash_enum(__hash_enum&&);
      |       ^~~~~~~~~~~
/usr/include/c++/14/bits/functional_hash.h:83:7: note:   candidate expects 1 argument, 0 provided
/usr/include/c++/14/bits/functional_hash.h:102:12: error: ‘std::__hash_enum<_Tp, <anonymous> >::~__hash_enum() [with _Tp = Version; bool <anonymous> = false]’ is private within this context
  102 |     struct hash : __hash_enum<_Tp>
      |            ^~~~
/usr/include/c++/14/bits/functional_hash.h:84:7: note: declared private here
   84 |       ~__hash_enum();
      |       ^
/usr/include/c++/14/bits/hashtable_policy.h:1245:49: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
 1245 |       _Hashtable_ebo_helper() noexcept(noexcept(_Tp())) : _Tp() { }
      |                                                 ^~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1328:7: note: ‘std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Hash, _RangeHash, _Unused, __cache_hash_code>::_Hash_code_base() [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _ExtractKey = std::__detail::_Select1st; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; bool __cache_hash_code = true]’ is implicitly deleted because the default definition would be ill-formed:
 1328 |       _Hash_code_base() = default;
      |       ^~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1328:7: error: use of deleted function ‘std::__detail::_Hashtable_ebo_helper<1, std::hash<Version>, true>::~_Hashtable_ebo_helper()’
/usr/include/c++/14/bits/hashtable_policy.h:1242:12: note: ‘std::__detail::_Hashtable_ebo_helper<1, std::hash<Version>, true>::~_Hashtable_ebo_helper()’ is implicitly deleted because the default definition would be ill-formed:
 1242 |     struct _Hashtable_ebo_helper<_Nm, _Tp, true>
      |            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1242:12: error: use of deleted function ‘std::hash<Version>::~hash()’
/usr/include/c++/14/bits/functional_hash.h:102:12: note: ‘std::hash<Version>::~hash()’ is implicitly deleted because the default definition would be ill-formed:
  102 |     struct hash : __hash_enum<_Tp>
      |            ^~~~
/usr/include/c++/14/bits/functional_hash.h:102:12: error: ‘std::__hash_enum<_Tp, <anonymous> >::~__hash_enum() [with _Tp = Version; bool <anonymous> = false]’ is private within this context
/usr/include/c++/14/bits/functional_hash.h:84:7: note: declared private here
   84 |       ~__hash_enum();
      |       ^
/usr/include/c++/14/bits/hashtable_policy.h:1731:7: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
 1731 |       _Hashtable_base() = default;
      |       ^~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1731:7: error: use of deleted function ‘std::__detail::_Hash_code_base<Version, std::pair<const Version, std::__cxx11::basic_string<char> >, std::__detail::_Select1st, std::hash<Version>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, true>::~_Hash_code_base()’
/usr/include/c++/14/bits/hashtable_policy.h:1306:12: note: ‘std::__detail::_Hash_code_base<Version, std::pair<const Version, std::__cxx11::basic_string<char> >, std::__detail::_Select1st, std::hash<Version>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, true>::~_Hash_code_base()’ is implicitly deleted because the default definition would be ill-formed:
 1306 |     struct _Hash_code_base
      |            ^~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1306:12: error: use of deleted function ‘std::__detail::_Hashtable_ebo_helper<1, std::hash<Version>, true>::~_Hashtable_ebo_helper()’
/usr/include/c++/14/bits/hashtable.h:539:7: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
  539 |       _Hashtable() = default;
      |       ^~~~~~~~~~
/usr/include/c++/14/bits/hashtable.h:539:7: error: use of deleted function ‘std::__detail::_Hashtable_base<Version, std::pair<const Version, std::__cxx11::basic_string<char> >, std::__detail::_Select1st, std::equal_to<Version>, std::hash<Version>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Hashtable_traits<true, false, true> >::~_Hashtable_base()’
/usr/include/c++/14/bits/hashtable_policy.h:1688:12: note: ‘std::__detail::_Hashtable_base<Version, std::pair<const Version, std::__cxx11::basic_string<char> >, std::__detail::_Select1st, std::equal_to<Version>, std::hash<Version>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Hashtable_traits<true, false, true> >::~_Hashtable_base()’ is implicitly deleted because the default definition would be ill-formed:
 1688 |     struct _Hashtable_base
      |            ^~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1688:12: error: use of deleted function ‘std::__detail::_Hash_code_base<Version, std::pair<const Version, std::__cxx11::basic_string<char> >, std::__detail::_Select1st, std::hash<Version>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, true>::~_Hash_code_base()’
/usr/include/c++/14/bits/hashtable.h:539:7: error: use of deleted function ‘constexpr std::_Enable_default_constructor<false, _Tag>::_Enable_default_constructor() [with _Tag = std::__detail::_Hash_node_base]’
  539 |       _Hashtable() = default;
      |       ^~~~~~~~~~
In file included from /usr/include/c++/14/bits/hashtable.h:36:
/usr/include/c++/14/bits/enable_special_members.h:113:15: note: declared here
  113 |     constexpr _Enable_default_constructor() noexcept = delete;
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable.h:539:7: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
  539 |       _Hashtable() = default;
      |       ^~~~~~~~~~
/usr/include/c++/14/bits/unordered_map.h:148:7: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
  148 |       unordered_map() = default;
      |       ^~~~~~~~~~~~~
main.cpp: In function ‘int main()’:
main.cpp:11:46: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
   11 |     std::unordered_map<Version, std::string> notes;
      |                                              ^~~~~
/usr/include/c++/14/bits/hashtable.h: In instantiation of ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::~_Hashtable() [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>]’:
/usr/include/c++/14/bits/unordered_map.h:109:11:   required from here
  109 |     class unordered_map
      |           ^~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable.h:1673:5: error: use of deleted function ‘std::__detail::_Hashtable_base<Version, std::pair<const Version, std::__cxx11::basic_string<char> >, std::__detail::_Select1st, std::equal_to<Version>, std::hash<Version>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Hashtable_traits<true, false, true> >::~_Hashtable_base()’
 1673 |     }
      |     ^
/usr/include/c++/14/bits/hashtable.h:1665:21: error: static assertion failed: Cache the hash code or qualify your functors involved in hash code and bucket index computation with noexcept
 1665 |       static_assert(noexcept(declval<const __hash_code_base_access&>()
      |                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 1666 |                         ._M_bucket_index(declval<const __node_value_type&>(),
      |                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 1667 |                                          (std::size_t)0)),
      |                                          ~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable.h:1665:21: note: ‘false’ evaluates to false
/usr/include/c++/14/bits/hashtable_policy.h: In instantiation of ‘std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Hash, _RangeHash, _Unused, __cache_hash_code>::__hash_code std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Hash, _RangeHash, _Unused, __cache_hash_code>::_M_hash_code(const _Key&) const [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _ExtractKey = std::__detail::_Select1st; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; bool __cache_hash_code = true; __hash_code = long unsigned int]’:
/usr/include/c++/14/bits/hashtable_policy.h:865:45:   required from ‘std::__detail::_Map_base<_Key, std::pair<const _Key, _Val>, _Alloc, std::__detail::_Select1st, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits, true>::mapped_type& std::__detail::_Map_base<_Key, std::pair<const _Key, _Val>, _Alloc, std::__detail::_Select1st, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits, true>::operator[](key_type&&) [with _Key = Version; _Val = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>; mapped_type = std::__cxx11::basic_string<char>; key_type = Version]’
  865 |       __hash_code __code = __h->_M_hash_code(__k);
      |                            ~~~~~~~~~~~~~~~~~^~~~~
/usr/include/c++/14/bits/unordered_map.h:992:20:   required from ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::mapped_type& std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::operator[](key_type&&) [with _Key = Version; _Tp = std::__cxx11::basic_string<char>; _Hash = std::hash<Version>; _Pred = std::equal_to<Version>; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; mapped_type = std::__cxx11::basic_string<char>; key_type = Version]’
  992 |       { return _M_h[std::move(__k)]; }
      |                ~~~~^
main.cpp:12:24:   required from here
   12 |     notes[Version{1, 2}] = "stable";
      |                        ^
/usr/include/c++/14/bits/hashtable_policy.h:1335:23: error: static assertion failed: hash function must be invocable with an argument of key type
 1335 |         static_assert(__is_invocable<const _Hash&, const _Key&>{},
      |                       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1335:23: note: ‘std::__is_invocable<const std::hash<Version>&, const Version&>()’ evaluates to false
/usr/include/c++/14/bits/hashtable_policy.h:1337:25: error: no match for call to ‘(const std::hash<Version>) (const Version&)’
 1337 |         return _M_hash()(__k);
      |                ~~~~~~~~~^~~~~
In file included from /usr/include/c++/14/string:49:
/usr/include/c++/14/bits/stl_function.h: In instantiation of ‘constexpr bool std::equal_to<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = Version]’:
/usr/include/c++/14/bits/hashtable_policy.h:1745:16:   required from ‘bool std::__detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _Traits>::_M_key_equals(const _Key&, const std::__detail::_Hash_node_value<_Value, typename _Traits::__hash_cached::value>&) const [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _Traits = std::__detail::_Hashtable_traits<true, false, true>; typename _Traits::__hash_cached = std::__detail::_Hashtable_traits<true, false, true>::__hash_cached]’
 1745 |         return _M_eq()(__k, _ExtractKey{}(__n._M_v()));
      |                ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:1764:39:   required from ‘bool std::__detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _Traits>::_M_equals(const _Key&, __hash_code, const std::__detail::_Hash_node_value<_Value, typename _Traits::__hash_cached::value>&) const [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _Traits = std::__detail::_Hashtable_traits<true, false, true>; __hash_code = long unsigned int; typename _Traits::__hash_cached = std::__detail::_Hashtable_traits<true, false, true>::__hash_cached]’
 1764 |       { return _S_equals(__c, __n) && _M_key_equals(__k, __n); }
      |                                       ^~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable.h:2078:23:   required from ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::__node_base_ptr std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::_M_find_before_node(size_type, const key_type&, __hash_code) const [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>; __node_base_ptr = std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<const Version, std::__cxx11::basic_string<char> >, true> > >::__node_base*; size_type = long unsigned int; key_type = Version; __hash_code = long unsigned int]’
 2078 |           if (this->_M_equals(__k, __code, *__p))
      |               ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable.h:822:50:   required from ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::__node_ptr std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::_M_find_node(size_type, const key_type&, __hash_code) const [with _Key = Version; _Value = std::pair<const Version, std::__cxx11::basic_string<char> >; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>; __node_ptr = std::allocator<std::__detail::_Hash_node<std::pair<const Version, std::__cxx11::basic_string<char> >, true> >::value_type*; size_type = long unsigned int; key_type = Version; __hash_code = long unsigned int]’
  822 |         __node_base_ptr __before_n = _M_find_before_node(__bkt, __key, __c);
      |                                      ~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/hashtable_policy.h:867:42:   required from ‘std::__detail::_Map_base<_Key, std::pair<const _Key, _Val>, _Alloc, std::__detail::_Select1st, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits, true>::mapped_type& std::__detail::_Map_base<_Key, std::pair<const _Key, _Val>, _Alloc, std::__detail::_Select1st, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits, true>::operator[](key_type&&) [with _Key = Version; _Val = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; _Equal = std::equal_to<Version>; _Hash = std::hash<Version>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, false, true>; mapped_type = std::__cxx11::basic_string<char>; key_type = Version]’
  867 |       if (auto __node = __h->_M_find_node(__bkt, __k, __code))
      |                         ~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/bits/unordered_map.h:992:20:   required from ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::mapped_type& std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::operator[](key_type&&) [with _Key = Version; _Tp = std::__cxx11::basic_string<char>; _Hash = std::hash<Version>; _Pred = std::equal_to<Version>; _Alloc = std::allocator<std::pair<const Version, std::__cxx11::basic_string<char> > >; mapped_type = std::__cxx11::basic_string<char>; key_type = Version]’
  992 |       { return _M_h[std::move(__k)]; }
      |                ~~~~^
main.cpp:12:24:   required from here
   12 |     notes[Version{1, 2}] = "stable";
      |                        ^
/usr/include/c++/14/bits/stl_function.h:375:20: error: no match for ‘operator==’ (operand types are ‘const Version’ and ‘const Version’)
  375 |       { return __x == __y; }
      |                ~~~~^~~~~~
In file included from /usr/include/c++/14/string:43:
/usr/include/c++/14/bits/allocator.h:235:5: note: candidate: ‘template<class _T1, class _T2> constexpr bool std::operator==(const allocator<_CharT>&, const allocator<_T2>&)’ (reversed)
  235 |     operator==(const allocator<_T1>&, const allocator<_T2>&)
      |     ^~~~~~~~
/usr/include/c++/14/bits/allocator.h:235:5: note:   template argument deduction/substitution failed:
/usr/include/c++/14/bits/stl_function.h:375:20: note:   ‘const Version’ is not derived from ‘const std::allocator<_CharT>’
  375 |       { return __x == __y; }
      |                ~~~~^~~~~~
In file included from /usr/include/c++/14/string:48:
/usr/include/c++/14/bits/stl_iterator.h:527:5: note: candidate: ‘template<class _IteratorL, class _IteratorR> constexpr bool std::operator==(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorR>&) requires requires{{std::operator==::__x->base() == std::operator==::__y->base()} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];}’ (reversed)
  527 |     operator==(const reverse_iterator<_IteratorL>& __x,
      |     ^~~~~~~~
/usr/include/c++/14/bits/stl_iterator.h:527:5: note:   template argument deduction/substitution failed:
/usr/include/c++/14/bits/stl_function.h:375:20: note:   ‘const Version’ is not derived from ‘const std::reverse_iterator<_IteratorL>’
  375 |       { return __x == __y; }
      |                ~~~~^~~~~~
/usr/include/c++/14/bits/stl_iterator.h:1667:5: note: candidate: ‘template<class _IteratorL, class _IteratorR> constexpr bool std::operator==(const move_iterator<_IteratorL>&, const move_iterator<_IteratorR>&) requires requires{{std::operator==::__x->base() == std::operator==::__y->base()} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];}’ (reversed)
 1667 |     operator==(const move_iterator<_IteratorL>& __x,
      |     ^~~~~~~~
/usr/include/c++/14/bits/stl_iterator.h:1667:5: note:   template argument deduction/substitution failed:
/usr/include/c++/14/bits/stl_function.h:375:20: note:   ‘const Version’ is not derived from ‘const std::move_iterator<_IteratorL>’
  375 |       { return __x == __y; }
      |                ~~~~^~~~~~
In file included from /usr/include/c++/14/iosfwd:42,
                 from /usr/include/c++/14/ios:40:
/usr/include/c++/14/bits/postypes.h:192:5: note: candidate: ‘template<class _StateT> bool std::operator==(const fpos<_StateT>&, const fpos<_StateT>&)’
  192 |     operator==(const fpos<_StateT>& __lhs, const fpos<_StateT>& __rhs)
      |     ^~~~~~~~
/usr/include/c++/14/bits/postypes.h:192:5: note:   template argument deduction/substitution failed:
/usr/include/c++/14/bits/stl_function.h:375:20: note:   ‘const Version’ is not derived from ‘const std::fpos<_StateT>’
  375 |       { return __x == __y; }
      |                ~~~~^~~~~~
/usr/include/c++/14/bits/stl_iterator.h:586:5: note: candidate: ‘template<class _Iterator> constexpr bool std::operator==(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorL>&) requires requires{{std::operator==::__x->base() == std::operator==::__y->base()} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];}’
  586 |     operator==(const reverse_iterator<_Iterator>& __x,
      |     ^~~~~~~~
/usr/include/c++/14/bits/stl_iterator.h:586:5: note:   template argument deduction/substitution failed:
/usr/include/c++/14/bits/stl_function.h:375:20: note:   ‘const Version’ is not derived from ‘const std::reverse_iterator<_IteratorL>’
  375 |       { return __x == __y; }
      |                ~~~~^~~~~~
/usr/include/c++/14/bits/stl_iterator.h:1737:5: note: candidate: ‘template<class _Iterator> constexpr bool std::operator==(const move_iterator<_IteratorL>&, const move_iterator<_IteratorL>&)’
 1737 |     operator==(const move_iterator<_Iterator>& __x,
      |     ^~~~~~~~
/usr/include/c++/14/bits/stl_iterator.h:1737:5: note:   template argument deduction/substitution failed:
/usr/include/c++/14/bits/stl_function.h:375:20: note:   ‘const Version’ is not derived from ‘const std::move_iterator<_IteratorL>’
  375 |       { return __x == __y; }
      |                ~~~~^~~~~~
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0277?
error[E0277]: the trait bound `Version: Eq` is not satisfied
  --> src/main.rs:10:11
   |
10 |     notes.insert(Version { major: 1, minor: 2 }, "stable");
   |           ^^^^^^ ------------------------------ `Version` doesn't satisfy the trait bound
   |           |
   |           the trait `Eq` is not implemented for `Version`
   |
help: consider annotating `Version` with `#[derive(Eq)]`
   |
 3 + #[derive(Eq)]
 4 | struct Version {
  ... (notes pointing into the standard library trimmed)

error[E0277]: the trait bound `Version: Hash` is not satisfied
  --> src/main.rs:10:11
   |
10 |     notes.insert(Version { major: 1, minor: 2 }, "stable");
   |           ^^^^^^ ------------------------------ `Version` doesn't satisfy the trait bound
   |           |
   |           the trait `Hash` is not implemented for `Version`
   |
help: consider annotating `Version` with `#[derive(Hash)]`
   |
 3 + #[derive(Hash)]
 4 | struct Version {
  ... (notes pointing into the standard library trimmed)

error[E0599]: the method `get` exists for struct `HashMap<Version, &str>`, but its trait bounds were not satisfied
  --> src/main.rs:11:23
   |
 3 | struct Version {
   | -------------- doesn't satisfy `Version: Eq` or `Version: Hash`
...
11 |     let found = notes.get(&Version { major: 1, minor: 2 });
   |                       ^^^ method cannot be called on `HashMap<Version, &str>` due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `Version: Eq`
           `Version: Hash`
help: consider annotating `Version` with `#[derive(Eq, Hash, PartialEq)]`
   |
 3 + #[derive(Eq, Hash, PartialEq)]
 4 | struct Version {
   |

Some errors have detailed explanations: E0277, E0599.
What happens in Go

Go prints found: stable true. A struct whose fields are all comparable can be a map key with nothing declared. A struct with a slice field cannot, and that is a compile error (the page shows it).

What happens in Kotlin

Kotlin prints found: stable, because a data class writes equals and hashCode. A plain class would not, and the lookup would find nothing (the page runs one).

What happens in Python

Python prints found: stable. A frozen dataclass writes __eq__ and __hash__. A plain class compares by identity, so the lookup finds nothing (the page runs one).

What happens in TypeScript

TypeScript compiles this and prints found: undefined. A Map compares object keys by identity, so the second new Version(1, 2) is a different key. Key on a string built from the fields, or keep the one object.

What happens in Java

Java prints found: stable, because a record writes equals and hashCode. A plain class without them finds nothing (the page runs one).

What happens in C#

C# prints found: stable, because a record writes Equals and GetHashCode. A plain class without them finds nothing (the page runs one).

What happens in C

C has no standard hash map type, so there is nothing to try. A table you write or borrow decides for itself how keys are hashed and compared.

What happens in C++

C++ refuses too. std::unordered_map needs a std::hash<Version>. The error says the unordered_map constructor is a deleted function, and it is long because it is reported from inside the library; look for std::hash<Version> in it. A std::map needs only an operator<.

What Rust sees

A HashMap key needs Eq and Hash, and Version has neither. Nothing is derived unless you ask. The compiler lists the missing traits and suggests the derive.

Why Rust says no

A key is compared and hashed, and for your own type Rust will not guess what "the same" means. Asking for #[derive(PartialEq, Eq, Hash)] says that two versions are the same when every field is. It is one line, and it keeps == and the hash in step.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
use std::collections::HashMap;
#[derive(PartialEq, Eq, Hash)]
struct Version {
major: u32,
minor: u32,
}
fn main() {
let mut notes = HashMap::new();
notes.insert(Version { major: 1, minor: 2 }, "stable");
let found = notes.get(&Version { major: 1, minor: 2 });
println!("found: {found:?}");
}
$ cargo run
found: Some("stable")

The toolkit

Outputs on this page come from Linux on 64-bit ARM with Rust 1.99.1.

A trait is a named ability a type can have (Traits is the full story). A handful of standard traits sit behind everyday syntax: ==, {:?}, .clone(), .sort(), .into(). The question is never “does my type work with ==?” but “did I give it the trait behind ==?” A new struct has none of them. #[derive(...)] asks the compiler to write the obvious version from the fields (Structs lists them); the rest you write by hand. Your language’s equivalents follow the table.

To do this, implement that

You want Implement
{:?} and {:#?}, and the message from assert_eq! Debug
{} and .to_string() Display
.clone() Clone
let b = a; without moving a Copy (needs Clone)
== and != PartialEq
<, >, <=, >= PartialOrd (needs PartialEq)
.sort(), a.max(b), .min(), a BTreeMap key Ord (needs Eq and PartialOrd)
a HashMap key, a HashSet item Eq and Hash
T::default(), ..Default::default() Default
T::from(x), x.into() From
T::try_from(x), x.try_into() TryFrom
"text".parse::<T>() FromStr
fn f(x: impl AsRef<str>) AsRef
a + b, -a, a += b, a[i] Add, Neg, AddAssign, Index
*x, a &String where a &str is wanted Deref
run code when a value goes out of scope Drop
for x in c and .next() IntoIterator and Iterator (Iterators)
? into a Box<dyn Error> Error (Errors)

derive fails if a field lacks the trait, and Display has no derive at all: there is no obvious text for a struct, so you choose it. Printing and Formatting shows how a Display you write should handle width.

Rustsrc/main.rs✗ won't compile
#[derive(Debug, Display)]
struct Version {
major: u32,
minor: u32,
}
fn main() {
let v = Version { major: 1, minor: 2 };
println!("{v:?}");
}
$ cargo build
error: cannot find derive macro `Display` in this scope
 --> src/main.rs:1:17
  |
1 | #[derive(Debug, Display)]
  |                 ^^^^^^^
  ... (notes pointing into the standard library trimmed)

Your language’s version

What the standard types have

Which standard types already have the traits you would want?

Type Display Copy Eq Ord Hash Default
f64 yes yes no no no yes
String yes no yes yes yes yes
Vec<T> no no if T if T if T yes
HashMap<K, V> no no if parts no no yes

i32, bool, char and &str have all six. Option<T> and tuples have no Display and take Copy, Eq, Ord and Hash from their parts. All of them have Debug, Clone and PartialEq (when their parts do). A derive needs every field to have the trait, so Vec<f64>, Option<f64> and any struct with an f64 field have no Eq, Ord or Hash.

When to derive, and when to write the impl yourself:

Trait Derive when Write it by hand when
Debug almost always a field must not be printed, such as a password
Clone each field’s own clone is right a copy should not copy everything, such as a cache
PartialEq, Hash equal means every field is equal equality ignores a field (and then Hash must ignore it too)
PartialOrd, Ord field order is the order you want the order is something else, such as most urgent first
Default every field’s default is a good start zero is wrong, such as a port of 0

Printing: Debug and Display

Debug is for you: logs, assert_eq! failures, a quick look. Display is for the people who use the program. Implementing Display also gives you .to_string(), so do not implement ToString yourself.

Rustsrc/main.rsRun ↗
use std::fmt;
#[derive(Debug)]
struct Version {
major: u32,
minor: u32,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "v{}.{}", self.major, self.minor)
}
}
struct Tag(String);
impl fmt::Display for Tag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(&self.0)
}
}
fn main() {
let v = Version { major: 1, minor: 2 };
println!("Display: {v}");
println!("Debug: {v:?}");
let text = v.to_string();
println!("to_string: {text}");
println!("width 8 with write!: [{v:>8}]");
println!("width 8 with pad: [{:>8}]", Tag(String::from("v1.2")));
}
$ cargo run
Display: v1.2
Debug: Version { major: 1, minor: 2 }
to_string: v1.2
width 8 with write!: [v1.2]
width 8 with pad:    [    v1.2]

The last two lines are a trap. write! ignores the >8 in {v:>8}: the flags are passed to your code, and it is your job to use them. f.pad does.

Clone and Copy

Clone is an explicit copy that may run any code. Copy is a promise that copying the bits gives a whole new value, so the compiler does it silently on every assignment (Ownership has the table of which types are Copy).

Rustsrc/main.rsRun ↗
struct Noisy(u32);
impl Clone for Noisy {
fn clone(&self) -> Noisy {
println!("cloning {}", self.0);
Noisy(self.0)
}
}
#[derive(Clone, Copy)]
struct Plain(u32);
fn main() {
let a = Noisy(1);
let b = a.clone();
println!("b: {}", b.0);
let c = b;
println!("moved, no clone: {}", c.0);
let list = vec![Noisy(10), Noisy(20)];
println!("cloning the list:");
let copy = list.clone();
println!("copy has {} items", copy.len());
let p = Plain(7);
let q = p;
println!("p and q both usable: {} {}", p.0, q.0);
}
$ cargo run
cloning 1
b: 1
moved, no clone: 1
cloning the list:
cloning 10
cloning 20
copy has 2 items
p and q both usable: 7 7

.clone() runs your code, a move runs nothing, and cloning a Vec calls clone once per item. Copy needs Clone, so derive both. A type with a destructor (Drop) can never be Copy:

Rustsrc/main.rs✗ won't compile
#[derive(Clone, Copy)]
struct Handle(u32);
impl Drop for Handle {
fn drop(&mut self) {
println!("closing {}", self.0);
}
}
fn main() {
let h = Handle(1);
let _again = h;
}
$ cargo buildWhat is E0184?
error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor
 --> src/main.rs:2:8
  |
1 | #[derive(Clone, Copy)]
  |                 ---- in this derive macro expansion
2 | struct Handle(u32);
  |        ^^^^^^ `Copy` not allowed on types with destructors
  |
note: destructor declared here
 --> src/main.rs:5:5
  |
5 |     fn drop(&mut self) {
  |     ^^^^^^^^^^^^^^^^^^

Equality, order and hashing

PartialEq gives ==. Eq has no methods: it is the promise that a == a is always true. f64 cannot make that promise, because NaN is not equal to itself. PartialOrd gives < and returns an Option<Ordering>, with None for values that cannot be compared. Ord is the full order that sort, max, min and BTreeMap need: a.max(b) on a type with only PartialOrd is a compile error (f64 has a max of its own).

A struct with an f64 field can derive PartialOrd but not Ord, so sort() is refused (Vectors shows a plain Vec<f64>):

Rustsrc/main.rs✗ won't compile
#[derive(Debug, PartialEq, PartialOrd)]
struct Reading {
sensor: u32,
value: f64,
}
fn main() {
let mut all = vec![
Reading {
sensor: 1,
value: 2.5,
},
Reading {
sensor: 2,
value: 0.5,
},
];
all.sort();
println!("{all:?}");
}
$ cargo buildWhat is E0277?
error[E0277]: the trait bound `Reading: Ord` is not satisfied
  --> src/main.rs:18:9
   |
18 |     all.sort();
   |         ^^^^ unsatisfied trait bound
   |
help: the trait `Ord` is not implemented for `Reading`
  --> src/main.rs:2:1
   |
 2 | struct Reading {
   | ^^^^^^^^^^^^^^
  ... (notes pointing into the standard library trimmed)

f64::total_cmp is a full order that includes NaN (a positive NaN sorts last), so it works with sort_by:

Rustsrc/main.rsRun ↗
#[derive(Debug, PartialEq, PartialOrd)]
struct Reading {
sensor: u32,
value: f64,
}
fn main() {
let mut all = vec![
Reading {
sensor: 1,
value: 2.5,
},
Reading {
sensor: 2,
value: f64::NAN,
},
Reading {
sensor: 3,
value: 0.5,
},
Reading {
sensor: 4,
value: -1.0,
},
];
let nan = f64::NAN;
println!("nan < 1.0: {}", nan < 1.0);
println!("nan > 1.0: {}", nan > 1.0);
println!("nan == nan: {}", nan == nan);
println!("partial_cmp: {:?}", nan.partial_cmp(&1.0));
println!("total_cmp: {:?}", nan.total_cmp(&1.0));
all.sort_by(|a, b| a.value.total_cmp(&b.value));
for r in &all {
println!("sensor {} value {}", r.sensor, r.value);
}
}
$ cargo run
nan < 1.0: false
nan > 1.0: false
nan == nan: false
partial_cmp: None
total_cmp: Greater
sensor 4 value -1
sensor 3 value 0.5
sensor 1 value 2.5
sensor 2 value NaN

A derived order compares fields in the order you wrote them, and enum variants by discriminant, which is declaration order unless you set the numbers. Moving a field changes the order, and nothing warns you. For another order, write Ord by hand: cmp returns an Ordering, and then_with breaks ties. PartialOrd just calls cmp.

Rustsrc/main.rsRun ↗
use std::cmp::{Ordering, Reverse};
// Derived: the fields are compared in the order they are written.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct ByRank {
rank: u8,
suit: char,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct BySuit {
suit: char,
rank: u8,
}
// Derived on an enum: the variants are ordered as they are declared.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Level {
Low,
Mid,
High,
}
// Written by hand: most urgent first, then by name.
#[derive(PartialEq, Eq)]
struct Task {
urgency: u8,
name: String,
}
impl Ord for Task {
fn cmp(&self, other: &Task) -> Ordering {
other
.urgency
.cmp(&self.urgency)
.then_with(|| self.name.cmp(&other.name))
}
}
impl PartialOrd for Task {
fn partial_cmp(&self, other: &Task) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn main() {
let (a, b) = ((1, 'z'), (2, 'a'));
println!(
"by rank first: a < b is {}",
ByRank {
rank: a.0,
suit: a.1
} < ByRank {
rank: b.0,
suit: b.1
}
);
println!(
"by suit first: a < b is {}",
BySuit {
suit: a.1,
rank: a.0
} < BySuit {
suit: b.1,
rank: b.0
}
);
println!("Low < High: {}", Level::Low < Level::High);
println!("Mid.cmp(Low): {:?}", Level::Mid.cmp(&Level::Low));
let mut tasks = vec![
Task {
urgency: 1,
name: String::from("sweep"),
},
Task {
urgency: 9,
name: String::from("deploy"),
},
Task {
urgency: 9,
name: String::from("alert"),
},
];
tasks.sort();
for t in &tasks {
println!("{} {}", t.urgency, t.name);
}
let mut nums = vec![3, 1, 2];
nums.sort_by_key(|&n| Reverse(n));
println!("reverse: {nums:?}");
}
$ cargo run
by rank first: a < b is true
by suit first: a < b is false
Low < High: true
Mid.cmp(Low): Greater
9 alert
9 deploy
1 sweep
reverse: [3, 2, 1]

Hash feeds a value into a hasher, an object the map supplies that mixes in what it is given and produces the number. hash returns nothing. A derive writes every field into the hasher. When your == ignores a field, write Hash by hand and feed the hasher only what == looks at, or two equal values hash differently and a map misplaces them:

Rustsrc/main.rsRun ↗
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::{BuildHasherDefault, Hash, Hasher};
// Two tickets are the same ticket when the id matches. The note does not count.
#[derive(Hash)]
struct Derived {
id: u32,
note: &'static str,
}
struct ByHand {
id: u32,
note: &'static str,
}
impl PartialEq for Derived {
fn eq(&self, other: &Derived) -> bool {
self.id == other.id
}
}
impl Eq for Derived {}
impl PartialEq for ByHand {
fn eq(&self, other: &ByHand) -> bool {
self.id == other.id
}
}
impl Eq for ByHand {}
// Nothing is returned: the parts that `eq` looks at are written into the hasher.
impl Hash for ByHand {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
fn hash_of<T: Hash>(t: &T) -> u64 {
let mut h = DefaultHasher::new();
t.hash(&mut h);
h.finish()
}
fn report<T: Hash + Eq>(label: &str, a: T, b: T) {
let same_hash = hash_of(&a) == hash_of(&b);
println!("{label}: equal {}, same hash {same_hash}", a == b);
// A fixed hasher, so every run prints the same thing.
let mut set: HashSet<T, BuildHasherDefault<DefaultHasher>> = HashSet::default();
set.insert(a);
println!("{label}: set contains the equal one: {}", set.contains(&b));
set.insert(b);
println!("{label}: set size {}", set.len());
}
fn main() {
let (a, b) = (
Derived {
id: 7,
note: "first",
},
Derived {
id: 7,
note: "second",
},
);
println!("notes: {} and {}", a.note, b.note);
report("derived Hash", a, b);
let (a, b) = (
ByHand {
id: 7,
note: "first",
},
ByHand {
id: 7,
note: "second",
},
);
println!("notes: {} and {}", a.note, b.note);
report("hand-written", a, b);
}
$ cargo run
notes: first and second
derived Hash: equal true, same hash false
derived Hash: set contains the equal one: false
derived Hash: set size 2
notes: first and second
hand-written: equal true, same hash true
hand-written: set contains the equal one: true
hand-written: set size 1

The derived version is equal and hashes differently, so the set misses the equal ticket and holds both. The docs call breaking the rule a logic error: no memory is unsafe, the answers are wrong. The set has a fixed hasher so the output is the same on every run; a normal one is randomly seeded (Maps). rustc says nothing about a derived Hash beside a hand-written PartialEq, but cargo clippy denies it (derived_hash_with_manual_eq, run on the derived half in the image, clippy 0.1.99). It cannot see two hand-written impls that disagree. A map key needs Eq and Hash (Maps); the gotcha shows the error when they are missing.

Starting from a default

Default makes a “blank” value. On an enum, #[default] picks the variant, and ..Default::default() fills the fields you leave out.

Rustsrc/main.rsRun ↗
#[derive(Default)]
struct Settings {
name: String,
retries: u32,
verbose: bool,
ratio: f64,
tags: Vec<String>,
limit: Option<u32>,
}
#[derive(Debug, Default, PartialEq)]
enum Mode {
Fast,
#[default]
Safe,
}
struct Server {
port: u16,
workers: u32,
}
// 0 is not a useful port, so this one is written by hand.
impl Default for Server {
fn default() -> Server {
Server {
port: 8080,
workers: 4,
}
}
}
fn main() {
let s = Settings::default();
println!(
"name {:?}, retries {}, verbose {}",
s.name, s.retries, s.verbose
);
println!("ratio {}, tags {:?}, limit {:?}", s.ratio, s.tags, s.limit);
println!(
"mode: {:?}, fast: {}",
Mode::default(),
Mode::default() == Mode::Fast
);
let custom = Settings {
retries: 3,
verbose: true,
..Default::default()
};
println!(
"retries {} verbose {} name {:?}",
custom.retries, custom.verbose, custom.name
);
let server = Server::default();
println!("port {}, workers {}", server.port, server.workers);
let missing: Option<u32> = None;
println!("unwrap_or_default: {}", missing.unwrap_or_default());
}
$ cargo run
name "", retries 0, verbose false
ratio 0, tags [], limit None
mode: Safe, fast: false
retries 3 verbose true name ""
port 8080, workers 4
unwrap_or_default: 0

Converting between types

Pick the conversion trait by asking what can go wrong:

The conversion Trait You call
cannot fail From<A> for B (you get Into free) B::from(a), a.into()
can fail TryFrom<A> for B B::try_from(a), a.try_into()
from text FromStr for B text.parse::<B>()
a cheap view of what you hold AsRef<B> for A a.as_ref()

Implement From and Into comes with it. A parameter of type impl Into<String> takes a &str or a String:

Rustsrc/main.rsRun ↗
#[derive(Debug)]
struct Rgb {
r: u8,
g: u8,
b: u8,
}
// Write From once ...
impl From<(u8, u8, u8)> for Rgb {
fn from(t: (u8, u8, u8)) -> Rgb {
Rgb {
r: t.0,
g: t.1,
b: t.2,
}
}
}
impl From<Rgb> for u32 {
fn from(c: Rgb) -> u32 {
((c.r as u32) << 16) | ((c.g as u32) << 8) | c.b as u32
}
}
// ... and Into comes with it. A parameter can ask for "anything that converts to String".
fn greet(name: impl Into<String>) -> String {
let name: String = name.into();
format!("hello, {name}")
}
fn main() {
let a = Rgb::from((255, 128, 0));
let b: Rgb = (0, 0, 255).into();
println!("{a:?} {b:?}");
let packed: u32 = a.into();
println!("packed: {packed:06x}");
println!("{}", greet("ana"));
println!("{}", greet(String::from("bo")));
let wide = i64::from(7_i32);
let letter = char::from(65_u8);
let code = u32::from('A');
println!("{wide} {letter} {code}");
}
$ cargo run
Rgb { r: 255, g: 128, b: 0 } Rgb { r: 0, g: 0, b: 255 }
packed: ff8000
hello, ana
hello, bo
7 A 65

.into() does not know the target on its own, so the type must be written somewhere:

Rustsrc/main.rs✗ won't compile
fn main() {
let small: u8 = 200;
let big = small.into();
println!("{big}");
}
$ cargo buildWhat is E0283?
error[E0283]: type annotations needed
 --> src/main.rs:3:9
  |
3 |     let big = small.into();
  |         ^^^         ---- type must be known at this point
  |
  = note: the type must implement `From<u8>`
  = note: required for `u8` to implement `Into<_>`
help: consider giving `big` an explicit type
  |
3 |     let big: /* Type */ = small.into();
  |            ++++++++++++

Std implements From between numbers only when no value can be lost. Your own From is only a promise not to fail, so keep it lossless (impl From<i64> that does n as u8 compiles, and 300 becomes 44). For the rest there is TryFrom, which returns a Result, and the as cast, which never fails and silently changes the value (Types has the rules):

Rustsrc/main.rsRun ↗
#[derive(Debug)]
struct Percent(u8);
#[derive(Debug)]
struct OutOfRange(i32);
impl TryFrom<i32> for Percent {
type Error = OutOfRange;
fn try_from(n: i32) -> Result<Percent, OutOfRange> {
if (0..=100).contains(&n) {
Ok(Percent(n as u8))
} else {
Err(OutOfRange(n))
}
}
}
fn show(n: i32) {
match Percent::try_from(n) {
Ok(p) => println!("{n} -> Percent({})", p.0),
Err(e) => println!("{n} -> rejected: {}", e.0),
}
}
fn main() {
println!("u8::try_from(200): {:?}", u8::try_from(200_i32));
let too_big = u8::try_from(300_i32).map_err(|e| e.to_string());
println!("u8::try_from(300): {too_big:?}");
println!("300 as u8: {}", 300_i32 as u8);
show(50);
show(101);
// try_into is the same call from the other side; the target type picks the impl.
let checked: Result<Percent, _> = (-5_i32).try_into();
println!("try_into -5: {checked:?}");
}
$ cargo run
u8::try_from(200): Ok(200)
u8::try_from(300): Err("number too large to fit in target type")
300 as u8: 44
50 -> Percent(50)
101 -> rejected: 101
try_into -5: Err(OutOfRange(-5))

FromStr is what parse calls. It works for your own types, and the error type is yours (Errors covers Result and ?, which also uses From to convert error types):

Rustsrc/main.rsRun ↗
use std::str::FromStr;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, PartialEq)]
enum ParsePointError {
Shape,
Number,
}
impl FromStr for Point {
type Err = ParsePointError;
fn from_str(s: &str) -> Result<Point, ParsePointError> {
let (x, y) = s.split_once(',').ok_or(ParsePointError::Shape)?;
let x = x.trim().parse().map_err(|_| ParsePointError::Number)?;
let y = y.trim().parse().map_err(|_| ParsePointError::Number)?;
Ok(Point { x, y })
}
}
fn main() {
println!("{:?}", "3, -4".parse::<Point>());
println!("{:?}", Point::from_str("7,8"));
println!("{:?}", "7;8".parse::<Point>());
println!("{:?}", "7,x".parse::<Point>());
// parse works for any type with FromStr, including the built-in ones
println!("{:?} {:?}", "42".parse::<u8>(), "-1".parse::<u8>());
println!("{:?}", "true".parse::<bool>());
}
$ cargo run
Ok(Point { x: 3, y: -4 })
Ok(Point { x: 7, y: 8 })
Err(Shape)
Err(Number)
Ok(42) Err(ParseIntError { kind: InvalidDigit })
Ok(true)

impl AsRef<str> lets a caller pass a String or a &str as it is to a function that only reads; impl Into<String> is for one that keeps the text:

Rustsrc/main.rsRun ↗
// &str: read-only, and a caller with a String must write &name.
fn shout_ref(s: &str) -> usize {
s.len()
}
// AsRef<str>: read-only, and the caller may pass a &str or a String as they are.
fn shout_asref(s: impl AsRef<str>) -> usize {
s.as_ref().len()
}
// Into<String>: the function keeps the text, so it needs an owned String.
fn keep(s: impl Into<String>) -> String {
s.into()
}
fn main() {
let owned = String::from("rust");
println!("&str: {} {}", shout_ref("rust"), shout_ref(&owned));
println!("AsRef: {} {}", shout_asref("rust"), shout_asref(&owned));
println!("AsRef moved the String: {}", shout_asref(owned));
println!("Into: {} {}", keep("rust"), keep(String::from("go")));
}
$ cargo run
&str: 4 4
AsRef: 4 4
AsRef moved the String: 4
Into: rust go

The conversions most code needs:

From To Write Fails?
&str String String::from(s), s.into(), s.to_string() no
i32 i64, f64 i64::from(x), f64::from(x) no
i64 i32 i32::try_from(x) yes
&str i32 s.parse::<i32>() yes
u8, u32 char char::from(b), char::try_from(n) u32: yes
Vec<T> [T; N] <[T; N]>::try_from(v) yes (wrong length)

Operators

An operator is a method call on a trait: a + b is Add::add(a, b). Implement the trait and the operator works. Output is the result type, and the right side can differ (Mul<f64>).

Rustsrc/main.rsRun ↗
use std::ops::{Add, AddAssign, Index, Mul, Neg};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
x: f64,
y: f64,
}
impl Add for Vec2 {
type Output = Vec2;
fn add(self, other: Vec2) -> Vec2 {
Vec2 {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl Mul<f64> for Vec2 {
type Output = Vec2;
fn mul(self, k: f64) -> Vec2 {
Vec2 {
x: self.x * k,
y: self.y * k,
}
}
}
impl Neg for Vec2 {
type Output = Vec2;
fn neg(self) -> Vec2 {
Vec2 {
x: -self.x,
y: -self.y,
}
}
}
impl AddAssign for Vec2 {
fn add_assign(&mut self, other: Vec2) {
self.x += other.x;
self.y += other.y;
}
}
struct Path(Vec<Vec2>);
impl Index<usize> for Path {
type Output = Vec2;
fn index(&self, i: usize) -> &Vec2 {
&self.0[i]
}
}
fn main() {
let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 10.0, y: 20.0 };
println!("a + b = {:?}", a + b);
println!("a * 3 = {:?}", a * 3.0);
println!("-a = {:?}", -a);
let mut c = a;
c += b;
println!("c = {:?}", c);
println!("a is still usable (Copy): {:?}", a);
let path = Path(vec![a, b, c]);
println!("path[1] = {:?}", path[1]);
}
$ cargo run
a + b = Vec2 { x: 11.0, y: 22.0 }
a * 3 = Vec2 { x: 3.0, y: 6.0 }
-a = Vec2 { x: -1.0, y: -2.0 }
c = Vec2 { x: 11.0, y: 22.0 }
a is still usable (Copy): Vec2 { x: 1.0, y: 2.0 }
path[1] = Vec2 { x: 10.0, y: 20.0 }
Operator Trait
+ - * / % Add Sub Mul Div Rem
-x, !x Neg, Not
& | ^ << >> BitAnd BitOr BitXor Shl Shr
+= and the rest AddAssign and one per operator
a[i] reads, a[i] = x writes Index, IndexMut

The arithmetic and bit operators take their operands by value, so a + b moves non-Copy operands. Vec2 is Copy, which is why a still works above. ==, < and a[i] borrow, and += takes &mut self.

Going deeper

The rules between traits

Some traits depend on each other, and the compiler cannot check some rules.

If you implement You must also have And you promise
Copy Clone copying the bits gives a complete new value
Eq PartialEq a == a
PartialOrd PartialEq < agrees with ==
Ord PartialOrd and Eq partial_cmp is Some(cmp)
Hash with Eq a == b means the same hash

From gives you Into, and Display gives you ToString, through blanket impls: one impl in std that covers every type meeting a bound (impl<T: Display> ToString for T; Generics shows the syntax).

Deref: one reference becomes another

String implements Deref<Target = str>, and Vec<T> implements Deref<Target = [T]>. When a function wants a &str and you hold a &String, the compiler calls deref for you. It is a deref coercion. A method of the target works on the outer type too, so v.first() is a slice method called on a Vec.

Rustsrc/main.rsRun ↗
use std::mem::size_of;
fn length(s: &str) -> usize {
s.len()
}
fn total(xs: &[i32]) -> i32 {
xs.iter().sum()
}
fn main() {
let owned = String::from("hello");
let text: &str = &owned; // &String becomes &str: String implements Deref<Target = str>
println!("length(&owned): {}", length(&owned));
println!("same bytes: {}", owned.as_ptr() == text.as_ptr());
println!(
"String is {} bytes, &str is {}",
size_of::<String>(),
size_of::<&str>()
);
println!("owned.len() {}, text.len() {}", owned.len(), text.len());
println!("capacity (String only): {}", owned.capacity());
let v = vec![1, 2, 3];
println!("total(&v): {}", total(&v)); // &Vec<i32> becomes &[i32]
println!("method through Deref: {:?}", v.first()); // first() is a slice method
}
$ cargo run
length(&owned): 5
same bytes: true
String is 24 bytes, &str is 16
owned.len() 5, text.len() 5
capacity (String only): 5
total(&v): 6
method through Deref: Some(1)
let owned = String::from("hello"); let text: &str = &owned;
STACKHEAPowned: Stringptrlen 5cap 5text: &strptrlen 5the same address as ownedthe texthello
The coercion copied no text. The &str is a pointer and a length into the bytes the String owns. (The order of a String's three fields is not promised.)

Implement Deref for your own type only when it is a smart pointer (Smart pointers). The Rust API Guidelines say so, because a wrapper that quietly forwards its inner type’s methods surprises readers. AsRef is the quieter way.

Drop: the trait behind scope exit

Drop runs code when a value goes out of scope: closing a file, releasing a lock (guards). Ownership covers the order for locals, list items and fields, and let _. Three more facts:

Rustsrc/main.rsRun ↗
struct Noisy(String);
impl Drop for Noisy {
fn drop(&mut self) {
println!(" drop {}", self.0);
}
}
fn noisy(name: &str) -> Noisy {
Noisy(String::from(name))
}
// A type with its own Drop and a field that has one too.
struct Connection {
log: Noisy,
stats: Noisy,
}
impl Drop for Connection {
fn drop(&mut self) {
println!(
" closing the connection (still holding {} and {})",
self.log.0, self.stats.0
);
}
}
fn main() {
println!("own Drop first, then the fields:");
{
let _conn = Connection {
log: noisy("log"),
stats: noisy("stats"),
};
println!(" end of block");
}
println!("a loop drops once per pass:");
for i in 0..2 {
let _guard = noisy(&format!("guard {i}"));
println!(" end of pass {i}");
}
println!("shadowing does not drop the old value:");
{
let v = noisy("old");
let v = Noisy(format!("{} -> new", v.0));
println!(" end of block, v is {}", v.0);
}
}
$ cargo run
own Drop first, then the fields:
  end of block
  closing the connection (still holding log and stats)
  drop log
  drop stats
a loop drops once per pass:
  end of pass 0
  drop guard 0
  end of pass 1
  drop guard 1
shadowing does not drop the old value:
  end of block, v is old -> new
  drop old -> new
  drop old
  • A type’s own drop runs first, then its fields are dropped. The fields are still there inside drop.
  • A value is dropped at the end of its scope, so a loop drops once per pass.
  • Shadowing a name does not drop the old value.

Practice

Try it

Each exercise has checks in main. Replace the todo!() and run it until it prints all checks passed.

Parse a version

Implement FromStr for Version: "1.10" is major 1, minor 10. Anything else is an error, including a missing dot, a third part, an empty part and a sign ("1.+2"). Parsing the text as a float would read "1.10" as 1.1, and u32::from_str alone accepts a +. The checks catch both.

Starterexercises/version-parse.rs
use std::str::FromStr;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
}
#[derive(Debug, PartialEq)]
struct ParseVersionError;
// "1.10" is major 1, minor 10. Anything else is an error:
// no dot, a third part, an empty part, or a part that is not plain digits (not even a sign).
impl FromStr for Version {
type Err = ParseVersionError;
fn from_str(s: &str) -> Result<Version, ParseVersionError> {
todo!("split_once('.'), then parse each side as a u32 (parse accepts a leading +)")
}
}
fn main() {
let v: Version = "1.10".parse().unwrap();
assert_eq!(
v,
Version {
major: 1,
minor: 10
}
); // not 1.1
assert_eq!(Version::from_str("0.0"), Ok(Version { major: 0, minor: 0 }));
let old: Version = "1.9".parse().unwrap();
assert!(old < v); // 9 < 10 as numbers
for bad in [
"", "1", "1.", ".2", "1.2.3", "a.b", "1.-2", "1.+2", "+1.2", "1,2",
] {
assert_eq!(bad.parse::<Version>(), Err(ParseVersionError), "{bad:?}");
}
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/version-parse.solution.rsRun ↗
use std::str::FromStr;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
}
#[derive(Debug, PartialEq)]
struct ParseVersionError;
// "1.10" is major 1, minor 10. Anything else is an error:
// no dot, a third part, an empty part, or a part that is not plain digits (not even a sign).
impl FromStr for Version {
type Err = ParseVersionError;
fn from_str(s: &str) -> Result<Version, ParseVersionError> {
let (major, minor) = s.split_once('.').ok_or(ParseVersionError)?;
Ok(Version {
major: digits(major)?,
minor: digits(minor)?,
})
}
}
// u32::from_str accepts a leading `+`, so check for digits first.
fn digits(s: &str) -> Result<u32, ParseVersionError> {
if !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(ParseVersionError);
}
s.parse().map_err(|_| ParseVersionError)
}
fn main() {
let v: Version = "1.10".parse().unwrap();
assert_eq!(
v,
Version {
major: 1,
minor: 10
}
); // not 1.1
assert_eq!(Version::from_str("0.0"), Ok(Version { major: 0, minor: 0 }));
let old: Version = "1.9".parse().unwrap();
assert!(old < v); // 9 < 10 as numbers
for bad in [
"", "1", "1.", ".2", "1.2.3", "a.b", "1.-2", "1.+2", "+1.2", "1,2",
] {
assert_eq!(bad.parse::<Version>(), Err(ParseVersionError), "{bad:?}");
}
println!("all checks passed");
}
$ cargo run
all checks passed

A name that ignores case

Implement PartialEq and Hash for Name so that "Ana", "ANA" and "aNa" are one key, and the map keeps the spelling it was given. Write hash by hand, as in the ticket example. A derived Hash next to your own eq breaks the rule, and the checks insert 256 spellings of one name to show it. (Python makes a class with __eq__ and no __hash__ unhashable; Rust lets you write the bug.)

Starterexercises/name-key.rs
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
// A name that ignores ASCII case: "Ana", "ANA" and "ana" are one key.
#[derive(Debug, Clone)]
struct Name(String);
impl PartialEq for Name {
fn eq(&self, other: &Name) -> bool {
todo!("compare without regard to ASCII case (eq_ignore_ascii_case)")
}
}
impl Eq for Name {}
impl Hash for Name {
fn hash<H: Hasher>(&self, state: &mut H) {
todo!("equal names must hash equally: feed the hasher something case-free")
}
}
// All 2^8 spellings of "abcdefgh" with some letters in capitals.
fn spellings() -> Vec<Name> {
(0..256)
.map(|mask: u32| {
let text: String = "abcdefgh"
.chars()
.enumerate()
.map(|(i, c)| {
if mask >> i & 1 == 1 {
c.to_ascii_uppercase()
} else {
c
}
})
.collect();
Name(text)
})
.collect()
}
fn main() {
assert_eq!(Name(String::from("Ana")), Name(String::from("aNA")));
assert_ne!(Name(String::from("Ana")), Name(String::from("Anna")));
assert_ne!(Name(String::from("Anab")), Name(String::from("Ana"))); // a prefix is not equal
assert_ne!(Name(String::from("Ana")), Name(String::from("Bob"))); // same length, other name
let mut seen = HashSet::new();
for name in spellings() {
seen.insert(name);
}
assert_eq!(seen.len(), 1); // one name, however it is spelled
seen.insert(Name(String::from("abcdefgi")));
assert_eq!(seen.len(), 2);
let mut ages = HashMap::new();
ages.insert(Name(String::from("Ana")), 30);
ages.insert(Name(String::from("Bo")), 25);
assert_eq!(ages.get(&Name(String::from("ANA"))), Some(&30));
assert_eq!(ages.get(&Name(String::from("bo"))), Some(&25));
assert_eq!(ages.get(&Name(String::from("cy"))), None);
assert_eq!(ages.len(), 2);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/name-key.solution.rsRun ↗
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
// A name that ignores ASCII case: "Ana", "ANA" and "ana" are one key.
#[derive(Debug, Clone)]
struct Name(String);
impl PartialEq for Name {
fn eq(&self, other: &Name) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for Name {}
impl Hash for Name {
fn hash<H: Hasher>(&self, state: &mut H) {
for b in self.0.bytes() {
b.to_ascii_lowercase().hash(state);
}
}
}
// All 2^8 spellings of "abcdefgh" with some letters in capitals.
fn spellings() -> Vec<Name> {
(0..256)
.map(|mask: u32| {
let text: String = "abcdefgh"
.chars()
.enumerate()
.map(|(i, c)| {
if mask >> i & 1 == 1 {
c.to_ascii_uppercase()
} else {
c
}
})
.collect();
Name(text)
})
.collect()
}
fn main() {
assert_eq!(Name(String::from("Ana")), Name(String::from("aNA")));
assert_ne!(Name(String::from("Ana")), Name(String::from("Anna")));
assert_ne!(Name(String::from("Anab")), Name(String::from("Ana"))); // a prefix is not equal
assert_ne!(Name(String::from("Ana")), Name(String::from("Bob"))); // same length, other name
let mut seen = HashSet::new();
for name in spellings() {
seen.insert(name);
}
assert_eq!(seen.len(), 1); // one name, however it is spelled
seen.insert(Name(String::from("abcdefgi")));
assert_eq!(seen.len(), 2);
let mut ages = HashMap::new();
ages.insert(Name(String::from("Ana")), 30);
ages.insert(Name(String::from("Bo")), 25);
assert_eq!(ages.get(&Name(String::from("ANA"))), Some(&30));
assert_eq!(ages.get(&Name(String::from("bo"))), Some(&25));
assert_eq!(ages.get(&Name(String::from("cy"))), None);
assert_eq!(ages.len(), 2);
println!("all checks passed");
}
$ cargo run
all checks passed

A percentage that checks its range

Implement TryFrom<i32> for Percent (0 to 100 only) and From<Percent> for f64. The field is public here, so Percent(200) still compiles; real code keeps it private in a module. Casting with as u8 before the range check lets 256, 300 and -255 through, because they wrap into range, and the checks include them.

Starterexercises/percent.rs
// A percentage is a whole number from 0 to 100.
#[derive(Debug, PartialEq)]
struct Percent(u8);
#[derive(Debug, PartialEq)]
struct OutOfRange;
impl TryFrom<i32> for Percent {
type Error = OutOfRange;
fn try_from(n: i32) -> Result<Percent, OutOfRange> {
todo!("accept 0..=100 only; do not cast with `as u8` before checking")
}
}
// 50 percent is 0.5.
impl From<Percent> for f64 {
fn from(p: Percent) -> f64 {
todo!("divide by 100.0")
}
}
fn main() {
assert_eq!(Percent::try_from(0), Ok(Percent(0)));
assert_eq!(Percent::try_from(100), Ok(Percent(100)));
assert_eq!(Percent::try_from(101), Err(OutOfRange));
assert_eq!(Percent::try_from(-1), Err(OutOfRange));
// These wrap into 0..=100 if you cast with `as u8` first: 256 -> 0, 300 -> 44, -255 -> 1.
assert_eq!(Percent::try_from(256), Err(OutOfRange));
assert_eq!(Percent::try_from(300), Err(OutOfRange));
assert_eq!(Percent::try_from(-255), Err(OutOfRange));
let p: Result<Percent, _> = 25_i32.try_into();
assert_eq!(p, Ok(Percent(25)));
assert_eq!(f64::from(Percent(50)), 0.5);
let ratio: f64 = Percent(100).into();
assert_eq!(ratio, 1.0);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/percent.solution.rsRun ↗
// A percentage is a whole number from 0 to 100.
#[derive(Debug, PartialEq)]
struct Percent(u8);
#[derive(Debug, PartialEq)]
struct OutOfRange;
impl TryFrom<i32> for Percent {
type Error = OutOfRange;
fn try_from(n: i32) -> Result<Percent, OutOfRange> {
match u8::try_from(n) {
Ok(small) if small <= 100 => Ok(Percent(small)),
_ => Err(OutOfRange),
}
}
}
// 50 percent is 0.5.
impl From<Percent> for f64 {
fn from(p: Percent) -> f64 {
f64::from(p.0) / 100.0
}
}
fn main() {
assert_eq!(Percent::try_from(0), Ok(Percent(0)));
assert_eq!(Percent::try_from(100), Ok(Percent(100)));
assert_eq!(Percent::try_from(101), Err(OutOfRange));
assert_eq!(Percent::try_from(-1), Err(OutOfRange));
// These wrap into 0..=100 if you cast with `as u8` first: 256 -> 0, 300 -> 44, -255 -> 1.
assert_eq!(Percent::try_from(256), Err(OutOfRange));
assert_eq!(Percent::try_from(300), Err(OutOfRange));
assert_eq!(Percent::try_from(-255), Err(OutOfRange));
let p: Result<Percent, _> = 25_i32.try_into();
assert_eq!(p, Ok(Percent(25)));
assert_eq!(f64::from(Percent(50)), 0.5);
let ratio: f64 = Percent(100).into();
assert_eq!(ratio, 1.0);
println!("all checks passed");
}
$ cargo run
all checks passed

Sort releases, pad the text

The starter sorts releases wrongly: a derived Ord compares the fields in the order they are written. Fix the order of the fields (or write Ord by hand), then write Display so that a width such as {:>8} is obeyed.

Starterexercises/release-order.rs
use std::fmt;
// Releases sort by major, then minor, then patch.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Release {
patch: u32,
minor: u32,
major: u32,
}
// "v1.2.3", and it must obey a width like {:>8} the way a number does.
impl fmt::Display for Release {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
todo!("build the text, then hand it to f.pad")
}
}
fn release(major: u32, minor: u32, patch: u32) -> Release {
Release {
major,
minor,
patch,
}
}
fn main() {
let mut all = vec![
release(1, 9, 1),
release(2, 0, 0),
release(1, 10, 0),
release(1, 9, 0),
];
all.sort();
let sorted: Vec<String> = all.iter().map(|r| r.to_string()).collect();
assert_eq!(sorted, ["v1.9.0", "v1.9.1", "v1.10.0", "v2.0.0"]);
assert!(release(1, 10, 0) > release(1, 9, 99));
let r = release(1, 2, 3);
assert_eq!(format!("{r}"), "v1.2.3");
assert_eq!(format!("[{r:>8}]"), "[ v1.2.3]");
assert_eq!(format!("[{r:<8}]"), "[v1.2.3 ]");
assert_eq!(format!("[{r:^8}]"), "[ v1.2.3 ]");
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/release-order.solution.rsRun ↗
use std::fmt;
// Releases sort by major, then minor, then patch.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Release {
major: u32,
minor: u32,
patch: u32,
}
// "v1.2.3", and it must obey a width like {:>8} the way a number does.
impl fmt::Display for Release {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(&format!("v{}.{}.{}", self.major, self.minor, self.patch))
}
}
fn release(major: u32, minor: u32, patch: u32) -> Release {
Release {
major,
minor,
patch,
}
}
fn main() {
let mut all = vec![
release(1, 9, 1),
release(2, 0, 0),
release(1, 10, 0),
release(1, 9, 0),
];
all.sort();
let sorted: Vec<String> = all.iter().map(|r| r.to_string()).collect();
assert_eq!(sorted, ["v1.9.0", "v1.9.1", "v1.10.0", "v2.0.0"]);
assert!(release(1, 10, 0) > release(1, 9, 99));
let r = release(1, 2, 3);
assert_eq!(format!("{r}"), "v1.2.3");
assert_eq!(format!("[{r:>8}]"), "[ v1.2.3]");
assert_eq!(format!("[{r:<8}]"), "[v1.2.3 ]");
assert_eq!(format!("[{r:^8}]"), "[ v1.2.3 ]");
println!("all checks passed");
}
$ cargo run
all checks passed

Read more

  • Derivable traits, the Rust Book’s appendix: what each derive generates and when it cannot.
  • The Rust Book on Deref and Drop, and Rust by Example on From and Into.
  • The std pages that state the rules: Hash (the Eq section) and Ord.