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
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());}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))}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}")}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))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);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()); }}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}";}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;}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";}display: v1.10 a == b: false a > b: true sorted: v0.0 v1.9 v1.10 distinct: 3
What's different
derivewrites the obvious code from the fields.Displaycannot be derived, because there is no obvious text: you writeimpl Display, andto_string()comes with it.Debug({:?}) is for developers andDisplay({}) is for users. A type has neither until you add it.- Some traits need others.
EqneedsPartialEq,OrdneedsPartialOrdandEq, andCopyneedsClone. f64hasPartialEqandPartialOrdbut notEq,OrdorHash, becauseNaNis not equal to itself. A struct with anf64field cannot derive them either.
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.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)}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)]}")}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)))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)));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))); }}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);found: stable
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. 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.
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.
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).
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).
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).
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.
Java prints found: stable, because a record writes equals and hashCode. A plain class without them finds nothing (the page runs one).
C# prints found: stable, because a record writes Equals and GetHashCode. A plain class without them finds nothing (the page runs one).
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. 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<.
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.
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.
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:?}");}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.
#[derive(Debug, Display)]struct Version { major: u32, minor: u32,}
fn main() { let v = Version { major: 1, minor: 2 }; println!("{v:?}");}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
Coming from C or C++
| In C and C++ | In Rust |
|---|---|
C: a == b on a struct |
#[derive(PartialEq)]; the C line does not compile |
C: qsort with a comparison returning an int |
Ord::cmp returns an Ordering; sort_by |
C++: operator== and operator<=> set to = default |
PartialEq and PartialOrd, by members in declaration order |
C++: std::strong_ordering, std::partial_ordering |
Ord, PartialOrd: a defaulted <=> is partial once a double is a member, as a derive is once an f64 is |
C++: a std::hash<T> specialisation and operator== |
Hash and Eq |
C++: a converting constructor that is not explicit |
From<U> for T; Rust converts only where you write .into() or T::from(x) (a &str for a String parameter is E0308) |
C++: copy constructor, operator<<, destructor |
Clone, Display, Drop |
C++ calls it the rule of zero when your members copy and destroy themselves; writing a destructor pulls you into the rules of three and five (copy, move, destroy together). In Rust that is derive(Clone) plus fields that drop themselves. There is no move constructor to write: a move is a bitwise copy after which the old name is dead and nothing runs on it. Clone runs only at .clone(), and Drop and Copy exclude each other (below), which the compiler enforces.
Coming from C
#include <stdio.h>
typedef struct { unsigned major; unsigned minor;} Version;
int main(void) { Version a = {1, 2}; Version b = {1, 2}; printf("equal: %d\n", a == b); return 0;}main.c: In function ‘main’:
main.c:11:29: error: invalid operands to binary == (have ‘Version’ and ‘Version’)
11 | printf("equal: %d\n", a == b);
| ^~Coming from C++
A std::unordered_map key needs a hash, usually a std::hash specialisation, and it must agree with operator== just as in Rust. It returns a size_t that you combine by hand (the * 31 + below). Rust’s hash returns nothing: it writes the fields into a hasher.
#include <functional>#include <iostream>#include <string>#include <unordered_map>
struct Version { unsigned major; unsigned minor; bool operator==(const Version &) const = default;};
// Hash: a specialisation of std::hash, kept in step with operator== by hand.template <>struct std::hash<Version> { size_t operator()(const Version &v) const { return std::hash<unsigned>()(v.major) * 31 + std::hash<unsigned>()(v.minor); }};
int main() { std::unordered_map<Version, std::string> notes; notes[Version{1, 2}] = "stable"; std::cout << "found: " << notes[Version{1, 2}] << "\n";}found: stable
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.
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")));}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).
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);}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:
#[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;}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>):
#[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:?}");}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:
#[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); }}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.
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:?}");}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:
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);}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.
#[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());}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:
#[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}");}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:
fn main() { let small: u8 = 200; let big = small.into(); println!("{big}");}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):
#[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:?}");}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):
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>());}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:
// &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")));}&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>).
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]);}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.
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}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)
&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:
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); }}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
dropruns first, then its fields are dropped. The fields are still there insidedrop. - 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.
Coming from Go
Go’s defer belongs to the function, not the scope. In a loop every deferred call waits until the function ends and then runs in reverse:
package main
import "fmt"
func main() { for i := 0; i < 2; i++ { defer fmt.Println("deferred", i) fmt.Println("end of iteration", i) } fmt.Println("end of function")}end of iteration 0 end of iteration 1 end of function deferred 1 deferred 0
Coming from Java, Kotlin or C#
Drop is not a finalizer: it runs at a known point, the closing brace, as the output above shows. To release a file on time in these languages you ask for it, with try-with-resources in Java, use in Kotlin or using in C#. Ending the block closes the resources in reverse order, which is Rust’s order too. A Rust value with a Drop needs no keyword:
public class Main { static class Noisy implements AutoCloseable { final String name;
Noisy(String name) { this.name = name; }
@Override public void close() { System.out.println(" close " + name); } }
public static void main(String[] args) { try (Noisy a = new Noisy("a"); Noisy b = new Noisy("b")) { System.out.println(" end of block"); } }}end of block close b close a
class Noisy(val name: String) : AutoCloseable { override fun close() = println(" close $name")}
fun main() { Noisy("a").use { a -> Noisy("b").use { b -> println(" end of block") } }}end of block close b close a
using (var a = new Noisy("a"))using (var b = new Noisy("b")){ Console.WriteLine(" end of block");}
class Noisy(string name) : IDisposable{ public void Dispose() => Console.WriteLine($" dispose {name}");}end of block dispose b dispose a
Coming from Python or TypeScript
Python’s counterpart is the with statement and TypeScript’s is using with Symbol.dispose. Each is asked for at every use, where Drop belongs to the type. Both close in reverse order, as Rust drops:
Coming from Python
class Noisy: def __init__(self, name): self.name = name
def __enter__(self): return self
def __exit__(self, *exc): print(" exit", self.name)
with Noisy("a") as a, Noisy("b") as b: print(" end of block")end of block exit b exit a
Coming from TypeScript
class Noisy { constructor(readonly name: string) {}
[Symbol.dispose]() { console.log(` dispose ${this.name}`); }}
{ using a = new Noisy("a"); using b = new Noisy("b"); console.log(" end of block");}end of block dispose b dispose a
Coming from C or C++
C++ destructors are Rust’s Drop, and locals go in the same reverse order. For members the order differs: C++ destroys them in reverse order of declaration, as second then first below, where Rust drops fields in declaration order (log, then stats above). C has no destructors; you call free yourself.
#include <iostream>#include <string>
struct Noisy { std::string name; explicit Noisy(std::string n) : name(std::move(n)) {} ~Noisy() { std::cout << " drop " << name << "\n"; }};
struct Pair { Noisy first{"first"}; Noisy second{"second"};};
int main() { std::cout << "locals:\n"; { Noisy a("a"); Noisy b("b"); Noisy c("c"); std::cout << " end of block\n"; } std::cout << "members:\n"; { Pair p; std::cout << " end of block\n"; }}locals: end of block drop c drop b drop a members: end of block drop second drop first
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.
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
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");}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.)
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
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");}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.
// 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
// 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");}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.
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
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");}all checks passed
Read more
- Derivable traits, the Rust Book’s appendix: what each derive generates and when it cannot.
- The Rust Book on
DerefandDrop, and Rust by Example onFromandInto. - The std pages that state the rules:
Hash(theEqsection) andOrd.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.