Rust for C and C++ developers
You already know how memory works. What changes is who checks the rules you keep in your head. This page takes ten habits and runs each one in the language it comes from, then in Rust. Each habit is labelled C, C++ or both.
Everything was compiled and run in the same Linux ARM64 image as the rest of the course: gcc and g++ 14 with -Wall -Wextra (C++ with -std=c++20), and Rust 1.99.1. Where a habit is about undefined behaviour, the build adds AddressSanitizer (-fsanitize=address, “ASan”) or UBSan (-fsanitize=undefined). Output with addresses or thread ids in it is marked as changing from run to run.
RememberRust keeps your picture of memory and moves three rules out of your head and into the compiler: who frees, who may alias, and what may be missing.
What carries over
Section titled “What carries over”- The memory model. Locals live on the stack,
Box,VecandStringown heap memory, and there is no garbage collector. Stack and heap will mostly confirm what you know. - Destructors. A type’s
Dropruns when its owner goes out of scope, which is what you use a C++ destructor for. Habit 1 prints it. - Templates become generics. The compiler makes one copy of a generic function per type, as it does for a template (One copy per type).
- Layout and linking.
#[repr(C)]asks for a layout that interoperates with C, andextern "C"functions link with C code (C and C++ interop). - Raw pointers exist. Dereferencing one needs an
unsafeblock, and the compiler says so:
error[E0133]: dereference of raw pointer is unsafe and requires unsafe block --> src/bin/unsafe_deref_fail.rs:4:20 | 4 | println!("{}", *p); | ^^ dereference of raw pointer | = note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior
Ten habits to unlearn
Section titled “Ten habits to unlearn”1. “I decide when to free” (both)
Section titled “1. “I decide when to free” (both)”In C, copying a pointer copies the address, so two free calls on one block are easy to write. gcc 14 warned (-Wuse-after-free) and still built it; ASan stopped the program. In C++, a new with a delete on only some paths leaks on the others, and LeakSanitizer (part of ASan on Linux) reports it.
In Rust a value is dropped once, when its owner goes out of scope, on whichever path leaves the scope: drop p prints before early: 1. After let b = a; there is one owner, so the second drop(a) is error E0382.
#include <stdio.h>#include <stdlib.h>#include <string.h>
int main(void) { char *a = malloc(6); strcpy(a, "hello"); char *b = a; /* copies the pointer, not the text */ printf("%s\n", b); free(a); free(b); /* the same block again */ return 0;}#include <iostream>
int main(int argc, char **) { int *p = new int(42); if (argc > 1) { return 1; // leaves early: the delete below never runs } std::cout << *p << "\n"; delete p; return 0;}struct Noisy(&'static str);
impl Drop for Noisy { fn drop(&mut self) { println!("drop {}", self.0); }}
fn leave_early(early: bool) -> i32 { let p = Box::new(Noisy("p")); if early { return 1; // p is dropped here, on this path too } println!("end of function, p holds {}", p.0); 0}
fn main() { println!("early: {}", leave_early(true)); println!("late: {}", leave_early(false)); let a = Box::new(Noisy("a")); let b = a; // a move: one owner, so one drop println!("moved, b holds {}", b.0);}fn main() { let a = String::from("hello"); let b = a; drop(a); drop(b);}$ gcc -Wall -Wextra -g -fsanitize=address c/h01_free.c -o h01_free && ./h01_free 2>&1 | head -10 # C c/h01_free.c: In function ‘main’: c/h01_free.c:11:5: warning: pointer ‘b’ used after ‘free’ [-Wuse-after-free] 11 | free(b); /* the same block again */ | ^~~~~~~ c/h01_free.c:10:5: note: call to ‘free’ here 10 | free(a); | ^~~~~~~ ================================================================= ==13==ERROR: AddressSanitizer: attempting double-free on 0x502000000010 in thread T0: #0 0xf53c0e2892b4 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52 #1 0xc20008600b6c in main c/h01_free.c:11 #2 0xf53c0e012258 (/lib/aarch64-linux-gnu/libc.so.6+0x22258) (BuildId: 5f3926d9963ff284b8173bf3601bbb00c3abad16) #3 0xf53c0e012338 in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x22338) (BuildId: 5f3926d9963ff284b8173bf3601bbb00c3abad16) #4 0xc20008600a2c in _start (/home/you/habits/h01_free+0xa2c) (BuildId: dff7fc14a002ce4d721c1e068f1c3a0a5e369fa9) 0x502000000010 is located 0 bytes inside of 6-byte region [0x502000000010,0x502000000016) freed by thread T0 here: $ g++ -std=c++20 -Wall -Wextra -g -fsanitize=address cpp/h01_free.cpp -o h01_free_cpp && ./h01_free_cpp x 2>&1 | head -10 # C++ ================================================================= ==21==ERROR: LeakSanitizer: detected memory leaks Direct leak of 4 byte(s) in 1 object(s) allocated from: #0 0xee23531eaf44 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95 #1 0xc7d4a5a60e7c in main cpp/h01_free.cpp:4 #2 0xee2352c12258 (/lib/aarch64-linux-gnu/libc.so.6+0x22258) (BuildId: 5f3926d9963ff284b8173bf3601bbb00c3abad16) #3 0xee2352c12338 in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x22338) (BuildId: 5f3926d9963ff284b8173bf3601bbb00c3abad16) #4 0xc7d4a5a60d6c in _start (/home/you/habits/h01_free_cpp+0xd6c) (BuildId: 1497a91584103ca0a5c6871f7c35d8c565c9aa79) $ cargo run --bin h01_drop drop p early: 1 end of function, p holds p drop p late: 0 moved, b holds a drop a $ cargo run --bin h01_double_free error[E0382]: use of moved value: `a` --> src/bin/h01_double_free.rs:4:10 | 2 | let a = String::from("hello"); | - move occurs because `a` has type `String`, which does not implement the `Copy` trait 3 | let b = a; | - value moved here 4 | drop(a); | ^ value used here after move | help: consider cloning the value if the performance cost is acceptable | 3 | let b = a.clone(); | ++++++++
This output changes from run to run. This is one real run.
Why: the compiler tracks one owner per value, so “who frees” is answered by the code. Read Ownership and Box, Rc and RefCell for the unique_ptr and shared_ptr counterparts.
2. “Indexing past the end is my problem” (both)
Section titled “2. “Indexing past the end is my problem” (both)”C and C++ do not check, and an out-of-range read is undefined behaviour. ASan named a stack-buffer-overflow for the C array and a heap-buffer-overflow for v[i]. In C++ v.at(i) is the checked form and throws.
In Rust a[i] is the checked form. It panics with the length and the index, and a.get(i) gives an Option instead. The release build panics too.
#include <stdio.h>
int main(int argc, char **argv) { (void)argv; int a[3] = {1, 2, 3}; int i = argc + 4; /* 5, but the compiler cannot see that */ printf("%d\n", a[i]); return 0;}#include <iostream>#include <vector>
int main(int argc, char **argv) { std::vector<int> v{1, 2, 3}; int i = static_cast<int>(v.size()) + 2; // 5 if (argc > 1 && argv[1][0] == 'a') { std::cout << v.at(i) << "\n"; // checked: throws } else { std::cout << v[i] << "\n"; // unchecked } return 0;}fn main() { let a = [1, 2, 3]; let i = std::env::args().count() + 4; // 5, known only at run time println!("get: {:?}", a.get(i)); println!("index: {}", a[i]);}$ gcc -Wall -Wextra -g -fsanitize=address c/h02_bounds.c -o h02_bounds && ./h02_bounds 2>&1 | head -4 # C ================================================================= ==88==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xed501bf00034 at pc 0xc682ef8f0d84 bp 0xffffe6faf540 sp 0xffffe6faf558 READ of size 4 at 0xed501bf00034 thread T0 #0 0xc682ef8f0d80 in main c/h02_bounds.c:7 $ g++ -std=c++20 -Wall -Wextra -g -fsanitize=address cpp/h02_bounds.cpp -o h02_bounds_cpp && ./h02_bounds_cpp 2>&1 | head -4 # C++, v[i] ================================================================= ==96==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000024 at pc 0xb4d5a716198c bp 0xffffef597bd0 sp 0xffffef597be8 READ of size 4 at 0x502000000024 thread T0 #0 0xb4d5a7161988 in main cpp/h02_bounds.cpp:10 $ ./h02_bounds_cpp at # C++, v.at(i) terminate called after throwing an instance of 'std::out_of_range' what(): vector::_M_range_check: __n (which is 5) >= this->size() (which is 3) $ cargo run --bin h02_bounds get: None thread 'main' (99) panicked at src/bin/h02_bounds.rs:5:27: index out of bounds: the len is 3 but the index is 5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace $ cargo run --release --bin h02_bounds get: None thread 'main' (134) panicked at src/bin/h02_bounds.rs:5:27: index out of bounds: the len is 3 but the index is 5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This output changes from run to run. This is one real run.
Why: a panic stops the thread with a message, and the bad read does not happen. See Vectors.
3. “A moved-from object is still there” (C++)
Section titled “3. “A moved-from object is still there” (C++)”std::move is a cast, and the standard library leaves a moved-from object “valid but unspecified”. libstdc++’s vector was empty in this run, and push_back on it was allowed. The worse case is a moved-from unique_ptr, which is null; dereferencing it crashed (Smart pointers).
A Rust move copies the bytes and the compiler forgets the old name. Using a afterwards is E0382, so there is no moved-from state to inspect.
#include <iostream>#include <utility>#include <vector>
int main() { std::vector<int> a{1, 2, 3}; std::vector<int> b = std::move(a); std::cout << "a.size() = " << a.size() << ", b.size() = " << b.size() << "\n"; a.push_back(9); // allowed: a is valid, its contents are unspecified std::cout << "a.size() = " << a.size() << "\n"; return 0;}fn main() { let a = vec![1, 2, 3]; let b = a; println!("a.len() = {}, b.len() = {}", a.len(), b.len());}$ g++ -std=c++20 -Wall -Wextra cpp/h03_move.cpp -o h03_move && ./h03_move # C++ a.size() = 0, b.size() = 3 a.size() = 1 $ cargo run --bin h03_move error[E0382]: borrow of moved value: `a` --> src/bin/h03_move.rs:4:44 | 2 | let a = vec![1, 2, 3]; | - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait 3 | let b = a; | - value moved here 4 | println!("a.len() = {}, b.len() = {}", a.len(), b.len()); | ^ value borrowed here after move | help: consider cloning the value if the performance cost is acceptable | 3 | let b = a.clone(); | ++++++++
Why: “used after move” is caught before the program runs. See Moves.
4. “A pointer into a container stays good” (both)
Section titled “4. “A pointer into a container stays good” (both)”A realloc may move the block (C), and push_back may reallocate the vector (C++). The pointer or reference taken before is then dangling. gcc warned about the C case; both runs ended in ASan’s heap-use-after-free.
Rust refuses at compile time: while first is alive, v may not be changed (E0502).
#include <stdio.h>#include <stdlib.h>
int main(void) { int *a = malloc(3 * sizeof *a); a[0] = 1; int *first = &a[0]; a = realloc(a, 1000 * sizeof *a); /* may move the block */ printf("%d\n", *first); free(a); return 0;}#include <iostream>#include <vector>
int main() { std::vector<int> v{1, 2, 3}; int &first = v[0]; v.push_back(4); // may reallocate std::cout << first << "\n"; return 0;}fn main() { let mut v = vec![1, 2, 3]; let first = &v[0]; v.push(4); println!("{first}");}$ gcc -Wall -Wextra -g -fsanitize=address c/h04_realloc.c -o h04_realloc && ./h04_realloc 2>&1 | head -12 # C c/h04_realloc.c: In function ‘main’: c/h04_realloc.c:9:5: warning: pointer ‘first’ used after ‘realloc’ [-Wuse-after-free] 9 | printf("%d\n", *first); | ^~~~~~~~~~~~~~~~~~~~~~ c/h04_realloc.c:8:9: note: call to ‘realloc’ here 8 | a = realloc(a, 1000 * sizeof *a); /* may move the block */ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ ================================================================= ==184==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 at pc 0xb404a9720cd8 bp 0xffffe0165ce0 sp 0xffffe0165cf8 READ of size 4 at 0x502000000010 thread T0 #0 0xb404a9720cd4 in main c/h04_realloc.c:9 #1 0xf159b1232258 (/lib/aarch64-linux-gnu/libc.so.6+0x22258) (BuildId: 5f3926d9963ff284b8173bf3601bbb00c3abad16) #2 0xf159b1232338 in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x22338) (BuildId: 5f3926d9963ff284b8173bf3601bbb00c3abad16) #3 0xb404a9720aec in _start (/home/you/habits/h04_realloc+0xaec) (BuildId: 6565b0c94647e3917264417046e0f46037e58b34) 0x502000000010 is located 0 bytes inside of 12-byte region [0x502000000010,0x50200000001c) freed by thread T0 here: #0 0xf159b14a94cc in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85 #1 0xb404a9720c74 in main c/h04_realloc.c:8 $ g++ -std=c++20 -Wall -Wextra -g -fsanitize=address cpp/h04_push_back.cpp -o h04_push_back && ./h04_push_back 2>&1 | head -4 # C++ ================================================================= ==192==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 at pc 0xc7d0c768189c bp 0xfffffdcffc10 sp 0xfffffdcffc28 READ of size 4 at 0x502000000010 thread T0 #0 0xc7d0c7681898 in main cpp/h04_push_back.cpp:8 $ cargo run --bin h04_push error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/bin/h04_push.rs:4:5 | 3 | let first = &v[0]; | - immutable borrow occurs here 4 | v.push(4); | ^^^^^^^^^ mutable borrow occurs here 5 | println!("{first}"); | ----- immutable borrow later used here
This output changes from run to run. This is one real run.
Why: Rust’s rule is many readers or one writer, never both, and it is checked at compile time; const T& and T& do not enforce it. See A reference into a list that moves and Working with the Borrow Checker.
5. “A missing value is a null pointer” (C)
Section titled “5. “A missing value is a null pointer” (C)”strchr returns NULL when there is no =, and eq + 1 then does arithmetic on null. ASan reported a read at address 0x1. C++’s std::optional is the same idea, but *o on an empty one is undefined behaviour (cppreference: until C++26, not run here); a Rust Option cannot be used as the value.
Rust’s find returns an Option<usize>. You must handle it, and eq + 1 is E0369.
#include <stdio.h>#include <string.h>
int main(void) { const char *line = "mode"; const char *eq = strchr(line, '='); /* NULL: there is no '=' */ printf("value: %s\n", eq + 1); return 0;}fn main() { let line = "mode"; match line.find('=') { Some(i) => println!("value: {}", &line[i + 1..]), None => println!("no '=' in {line:?}"), }}fn main() { let line = "mode"; let eq = line.find('='); println!("value starts at {}", eq + 1);}$ gcc -Wall -Wextra -g -fsanitize=address c/h05_null.c -o h05_null && ./h05_null 2>&1 | head -4 # C AddressSanitizer:DEADLYSIGNAL ================================================================= ==215==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000001 (pc 0xeb1e3b853b68 bp 0xfffffa1fab50 sp 0xfffffa1fab50 T0) ==215==The signal is caused by a READ memory access. $ cargo run --bin h05_option no '=' in "mode" $ cargo run --bin h05_option_fail error[E0369]: cannot add `{integer}` to `Option<usize>` --> src/bin/h05_option_fail.rs:4:39 | 4 | println!("value starts at {}", eq + 1); | -- ^ - {integer} | | | Option<usize> ... (notes pointing into the standard library trimmed)
This output changes from run to run. This is one real run.
Why: absence is part of the type, so forgetting to check is a compile error. See Option.
6. “Declare it first, put it in a header” (both)
Section titled “6. “Declare it first, put it in a header” (both)”gcc 14 rejects a call to a function it has not seen (GCC 14 porting guide), and g++ says 'square' was not declared in this scope. Rust items can be used above their definition, and there are no prototypes, headers or include guards: mod names a node in a tree of code and does not paste text.
What you do have to write is pub. Items are private by default, so the pub keyword does the job of listing a function in a header (E0603 below).
#include <stdio.h>
int main(void) { printf("%d\n", square(7)); return 0;}
int square(int x) { return x * x; }#include <iostream>
int main() { std::cout << square(7) << "\n"; return 0;}
int square(int x) { return x * x; }fn main() { println!("{}", square(7)); println!("{}", shapes::area(3, 4));}
fn square(x: i32) -> i32 { x * x}
mod shapes { pub fn area(w: i32, h: i32) -> i32 { w * h }}mod shapes { fn area(w: i32, h: i32) -> i32 { w * h }}
fn main() { println!("{}", shapes::area(3, 4));}$ gcc -Wall -Wextra c/h06_order.c -o h06_order # C c/h06_order.c: In function ‘main’: c/h06_order.c:4:20: error: implicit declaration of function ‘square’ [-Wimplicit-function-declaration] 4 | printf("%d\n", square(7)); | ^~~~~~ $ g++ -std=c++20 -Wall -Wextra cpp/h06_order.cpp -o h06_order_cpp # C++ cpp/h06_order.cpp: In function ‘int main()’: cpp/h06_order.cpp:4:18: error: ‘square’ was not declared in this scope 4 | std::cout << square(7) << "\n"; | ^~~~~~ $ cargo run --bin h06_order 49 12 $ cargo run --bin h06_private error[E0603]: function `area` is private --> src/bin/h06_private.rs:8:28 | 8 | println!("{}", shapes::area(3, 4)); | ^^^^ private function | note: the function `area` is defined here --> src/bin/h06_private.rs:2:5 | 2 | fn area(w: i32, h: i32) -> i32 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Why: the crate is compiled as a whole. See Private by default and the step-by-step Files and modules.
7. “Variables change unless I write const” (both)
Section titled “7. “Variables change unless I write const” (both)”Both C and C++ printed count = 2. In Rust let is the const one, and E0384 says so; the compiler’s help text suggests let mut.
#include <stdio.h>
int main(void) { int count = 1; /* mutable unless you write const */ count = count + 1; printf("count = %d\n", count); return 0;}#include <iostream>
int main() { auto count = 1; // mutable unless you write const count = count + 1; std::cout << "count = " << count << "\n"; return 0;}fn main() { let count = 1; count = count + 1; println!("count = {count}");}fn main() { let mut count = 1; count = count + 1; println!("count = {count}");}$ gcc -Wall -Wextra c/h07_const.c -o h07_const && ./h07_const # C count = 2 $ g++ -std=c++20 -Wall -Wextra cpp/h07_const.cpp -o h07_const_cpp && ./h07_const_cpp # C++ count = 2 $ cargo run --bin h07_let error[E0384]: cannot assign twice to immutable variable `count` --> src/bin/h07_let.rs:3:5 | 2 | let count = 1; | ----- first assignment to `count` 3 | count = count + 1; | ^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable | help: consider making this binding mutable | 2 | let mut count = 1; | +++ $ cargo run --bin h07_let_mut count = 2
Why: a reader sees which names can change by looking for mut. See Frozen unless you say mut.
8. “A macro or a template will do” (C for macros, C++ for templates)
Section titled “8. “A macro or a template will do” (C for macros, C++ for templates)”MAX(i++, j) pastes i++ twice, so i ends at 7 and m is 6; gcc 14 said nothing. A C++ template is checked after the type is substituted, so the error for a Point with no > appears inside the function body, with “required from here” pointing back at the call.
Rust checks a generic body once, against its bound. Without T: PartialOrd the error (E0369) is at the definition, and an argument expression runs once: i and m are both 6.
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) { int i = 5, j = 3; int m = MAX(i++, j); /* expands to ((i++) > (j) ? (i++) : (j)) */ printf("m = %d, i = %d\n", m, i); return 0;}#include <iostream>
template <typename T>T largest(T a, T b) { return a > b ? a : b;}
struct Point { int x, y; };
int main() { std::cout << largest(3, 5) << "\n"; largest(Point{1, 2}, Point{3, 4}); return 0;}fn largest<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b }}
fn next(counter: &mut i32) -> i32 { *counter += 1; *counter}
fn main() { let mut i = 5; let j = 3; let m = largest(next(&mut i), j); // next runs once println!("m = {m}, i = {i}"); println!("{}", largest("pear", "apple"));}fn largest<T>(a: T, b: T) -> T { if a > b { a } else { b }}
fn main() { println!("{}", largest(3, 5));}$ gcc -Wall -Wextra c/h08_macro.c -o h08_macro && ./h08_macro # C m = 6, i = 7 $ g++ -std=c++20 -Wall -Wextra cpp/h08_template.cpp -o h08_template # C++ cpp/h08_template.cpp: In instantiation of ‘T largest(T, T) [with T = Point]’: cpp/h08_template.cpp:12:12: required from here 12 | largest(Point{1, 2}, Point{3, 4}); | ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~ cpp/h08_template.cpp:5:14: error: no match for ‘operator>’ (operand types are ‘Point’ and ‘Point’) 5 | return a > b ? a : b; | ~~^~~ $ cargo run --bin h08_generic m = 6, i = 6 pear $ cargo run --bin h08_no_bound error[E0369]: binary operation `>` cannot be applied to type `T` --> src/bin/h08_no_bound.rs:2:10 | 2 | if a > b { a } else { b } | - ^ - T | | | T | help: consider restricting type parameter `T` with trait `PartialOrd` | 1 | fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T { | ++++++++++++++++++++++
Why: the bound is the contract. Reach for a macro when a generic function cannot do the job. See Bounds: what T may do and Macros you’ll read.
9. “Integers convert and overflow quietly” (both)
Section titled “9. “Integers convert and overflow quietly” (both)”In C and C++, -1 < 1u is false because the int is converted to unsigned (-Wextra warned). uint8_t arithmetic happens in int, so 200 + 100 is 300 and storing it back keeps 44. Signed overflow is undefined behaviour, and UBSan reported it.
Rust has no implicit numeric conversions (E0308), and the checked, wrapping and widening versions are separate methods. A debug build panics on overflow; the release run wrapped to 44. Cargo’s profile settings list overflow-checks as on for dev and off for release.
#include <limits.h>#include <stdint.h>#include <stdio.h>
int main(int argc, char **argv) { (void)argv; int x = -1; unsigned int y = 1; printf("-1 < 1u is %s\n", x < y ? "true" : "false");
uint8_t a = 200, b = 100; uint8_t c = a + b; /* a + b is an int: 300, cut to 8 bits */ printf("200 + 100 = %d, stored in 8 bits: %d\n", a + b, c);
int big = INT_MAX - 1 + argc; /* INT_MAX */ printf("INT_MAX + 1 = %d\n", big + 1); return 0;}#include <limits.h>#include <stdint.h>#include <stdio.h>
int main(int argc, char **argv) { (void)argv; int x = -1; unsigned int y = 1; printf("-1 < 1u is %s\n", x < y ? "true" : "false");
uint8_t a = 200, b = 100; uint8_t c = a + b; /* a + b is an int: 300, cut to 8 bits */ printf("200 + 100 = %d, stored in 8 bits: %d\n", a + b, c);
int big = INT_MAX - 1 + argc; /* INT_MAX */ printf("INT_MAX + 1 = %d\n", big + 1); return 0;}fn main() { let x: i32 = -1; let y: u32 = 1; println!("-1 < 1 is {}", x < y);}fn main() { let args = std::env::args().count() as u8; // 1: known only at run time let a: u8 = 199 + args; let b: u8 = 100; println!("checked: {:?}", a.checked_add(b)); println!("wrapping: {}", a.wrapping_add(b)); println!("widened: {}", u16::from(a) + u16::from(b)); println!("plain: {}", a + b);}$ gcc -Wall -Wextra -fsanitize=undefined c/h09_conversions.c -o h09_c && ./h09_c # C c/h09_conversions.c: In function ‘main’: c/h09_conversions.c:9:33: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare] 9 | printf("-1 < 1u is %s\n", x < y ? "true" : "false"); | ^ -1 < 1u is false 200 + 100 = 300, stored in 8 bits: 44 c/h09_conversions.c:16:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' INT_MAX + 1 = -2147483648 $ g++ -std=c++20 -Wall -Wextra -fsanitize=undefined cpp/h09_conversions.cpp -o h09_cpp && ./h09_cpp # C++ cpp/h09_conversions.cpp: In function ‘int main(int, char**)’: cpp/h09_conversions.cpp:9:33: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare] 9 | printf("-1 < 1u is %s\n", x < y ? "true" : "false"); | ~~^~~ -1 < 1u is false 200 + 100 = 300, stored in 8 bits: 44 cpp/h09_conversions.cpp:16:11: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' INT_MAX + 1 = -2147483648 $ cargo run --bin h09_compare_fail error[E0308]: mismatched types --> src/bin/h09_compare_fail.rs:4:34 | 4 | println!("-1 < 1 is {}", x < y); | - ^ expected `i32`, found `u32` | | | expected because this is `i32` | help: you can convert a `u32` to an `i32` and panic if the converted value doesn't fit | 4 | println!("-1 < 1 is {}", x < y.try_into().unwrap()); | ++++++++++++++++++++
$ cargo run --bin h09_overflow checked: None wrapping: 44 widened: 300 thread 'main' (454) panicked at src/bin/h09_overflow.rs:8:27: attempt to add with overflow note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace $ cargo run --release --bin h09_overflow checked: None wrapping: 44 widened: 300 plain: 44
This output changes from run to run. This is one real run.
Why: every numeric conversion is written down, and the one that loses information is a method you can search for. See Overflow and No implicit widening.
10. “Errors are return codes or exceptions” (both)
Section titled “10. “Errors are return codes or exceptions” (both)”C’s atoi("abc") returns 0, the same as atoi("0"). C++’s std::stoi throws, and with no catch the program aborts. Nothing in either signature says so.
Rust’s parse returns a Result. ? passes the error up, and a main that returns Err prints it and exits with status 1 (task 3 below shows the code).
#include <stdio.h>#include <stdlib.h>
int main(void) { int n = atoi("abc"); /* no way to tell "0" from "not a number" */ printf("n = %d\n", n); return 0;}#include <iostream>#include <string>
int main() { int n = std::stoi("abc"); // throws; nothing in the type says so std::cout << "n = " << n << "\n"; return 0;}use std::num::ParseIntError;
fn double(text: &str) -> Result<i32, ParseIntError> { let n: i32 = text.parse()?; Ok(n * 2)}
fn main() -> Result<(), ParseIntError> { println!("{:?}", double("21")); println!("{:?}", double("abc")); let n = double("abc")?; println!("not reached: {n}"); Ok(())}$ gcc -Wall -Wextra c/h10_errors.c -o h10_c && ./h10_c # C n = 0 $ g++ -std=c++20 -Wall -Wextra cpp/h10_errors.cpp -o h10_cpp && ./h10_cpp # C++ terminate called after throwing an instance of 'std::invalid_argument' what(): stoi $ cargo run --bin h10_result Ok(42) Err(ParseIntError { kind: InvalidDigit }) Error: ParseIntError { kind: InvalidDigit }
Why: the failure is part of the return type, so the compiler warns a caller who ignores it. See Errors.
Idiom by idiom
Section titled “Idiom by idiom”Every cell below is a line from a program that compiled and ran: idioms.c, idioms.cpp and idioms.rs, whose output follows the table.
| You write (C / C++) | In Rust | Read |
|---|---|---|
C int *p = malloc(sizeof *p); then free(p);C++ auto p = std::make_unique<int>(7); |
let p = Box::new(7); |
Smart pointers |
C++ auto s = std::make_shared<int>(7); |
let s = Rc::new(7);, or let a = Arc::new(7); across threads |
Smart pointers |
C++ std::vector<int> v{1, 2, 3}; and v.push_back(4); |
let mut v = vec![1, 2, 3]; and v.push(4); |
Vectors |
C++ v.at(9) (throws) |
v.get(9) (an Option) |
Vectors |
C++ std::string s = "hi"; and s += " there"; |
let mut s = String::from("hi"); and s.push_str(" there"); |
Strings |
C const char *s = "hi";C++ std::string_view s = "hi"; |
let s: &str = "hi"; |
Strings |
C p != NULLC++ std::optional<int> o = 5; |
let o: Option<i32> = Some(5); and if let Some(x) = o |
Option |
C++ std::variant<int, std::string> and std::visit |
enum Value { Int(i32), Text(String) } and match w |
Enums |
C strtol(text, &end, 10)C++ std::stoi("abc") in a try block |
"abc".parse::<i32>(), a Result |
Errors |
C++ for (const auto &x : v) |
for x in &v |
Looping over collections |
C++ std::views::transform(std::views::filter(v, even), times_ten) |
v.iter().filter(|x| **x % 2 == 0).map(|x| x * 10).collect() |
Iterators |
C++ m["a"]++; |
*m.entry("a").or_insert(0) += 1; |
Maps |
C++ std::unique_ptr<Shape> sh = std::make_unique<Square>(3); |
let sh: Box<dyn Shape> = Box::new(Square(3.0)); |
Trait objects |
C++ template <std::totally_ordered T> |
fn largest<T: PartialOrd>(a: T, b: T) -> T, the closest bound; Ord is the total-order one, and f64 lacks it |
Generics |
C++ ~Guard() { |
impl Drop for Guard { |
Drop |
C++ std::lock_guard<std::mutex> g(m); |
let n = Mutex::new(0); and *n.lock().unwrap() += 1; |
Shared state |
C pthread_create(&t, NULL, work, &result);C++ std::thread t([&result] { result = 42; }); |
thread::spawn(|| 42).join().unwrap() |
Threads |
C (int)xC++ static_cast<int>(x) |
x as i32, or i32::try_from(big) |
Casts |
C #define LIMIT 100C++ constexpr int limit = 100; |
const LIMIT: i32 = 100; |
const, static and let |
C int *p = &x; and *p |
let p = &x as *const i32; and unsafe { *p } |
unsafe |
$ gcc -Wall -Wextra -pthread c/idioms.c -o idioms_c && ./idioms_c # C 1 7 6 2 7 NULL 9 no digits (n=0) 17 42 18 3 19 100 20 5 $ g++ -std=c++20 -Wall -Wextra -pthread cpp/idioms.cpp -o idioms_cpp && ./idioms_cpp # C++ 1 7 2 7 owners=2 3 4 4 out_of_range 5 hi there 6 2 7 5 8 5 8 five 9 invalid_argument 10 10 11 20 40 12 2 13 9 14 5 15 bye 16 1 17 42 18 3 19 100 $ cargo run --bin idioms 1 7 2 7 owners=2 1 3 4 4 None 5 hi there 6 2 7 5 8 5 8 five 9 invalid digit found in string 10 10 11 [20, 40] 12 2 13 9 14 5 15 bye 16 1 17 42 18 3 true 19 100 20 5
Toolchain map
Section titled “Toolchain map”| Job | C and C++ | Rust |
|---|---|---|
| Build | CMake with Make (installed in the session below) | cargo build, cargo run |
| Dependencies | vcpkg, Conan or system packages (named, not run) | Cargo.toml, crates.io and Cargo.lock (Using crates) |
| Test | CTest, GoogleTest (named, not run) | cargo test (Testing) |
| Format | clang-format (not in the image, not run) | cargo fmt |
| Lint | -Wall -Wextra (run throughout), clang-tidy and cppcheck (not in the image, not run) |
cargo clippy |
| Memory errors | -fsanitize=address,undefined (run throughout) |
Miri, installed with rustup +nightly component add miri (README); the toolchain in the image has no miri component, as the last session below shows (not run) |
| Docs | Doxygen (named, not run) | cargo doc, and doc comments are tests |
| Debugger | gdb, lldb (not in the image, not run) | rust-gdb and rust-lldb are present in the image (last session below), but gdb and lldb are not, so no debugger was run |
The first session below shows that CMake and Make are not in the image, then installs them (needs network). The same “hello” is then built both ways:
cmake_minimum_required(VERSION 3.20)project(hello CXX)
set(CMAKE_CXX_STANDARD 20)set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(hello main.cpp)#include <iostream>
int main() { std::cout << "hello from C++\n"; return 0;}[package]name = "hello"version = "0.1.0"edition = "2024"
[dependencies]/// Adds two numbers.////// ```/// assert_eq!(hello::add(2, 3), 5);/// ```pub fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn adds_small_numbers() { assert_eq!(add(2, 3), 5); }}fn main() { println!("2 + 3 = {}", hello::add(2, 3));}$ for t in cmake make; do command -v $t ...; done cmake: not found make: not found $ apt-get install cmake make # needs network $ cmake -S . -B build -- The CXX compiler identification is GNU 14.2.0 -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done (0.1s) -- Generating done (0.0s) -- Build files have been written to: /home/you/habits/build $ cmake --build build [ 50%] Building CXX object CMakeFiles/hello.dir/main.cpp.o [100%] Linking CXX executable hello [100%] Built target hello $ ./build/hello hello from C++
This output changes from run to run. This is one real run.
$ cargo run 2 + 3 = 5 $ cargo test running 1 test test tests::adds_small_numbers ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests hello running 1 test test src/lib.rs - add (line 3) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s all doctests ran in 0.16s; merged doctests compilation took 0.15s $ cargo fmt --check $ cargo clippy $ cargo doc --no-deps Documenting hello v0.1.0 (/home/you/habits) Generated target/doc/hello/index.html
$ for t in gcc g++ gdb lldb valgrind clang-format clang-tidy cppcheck cargo rust-gdb rust-lldb; do command -v $t ...; done gcc: found g++: found gdb: not found lldb: not found valgrind: not found clang-format: not found clang-tidy: not found cppcheck: not found cargo: found rust-gdb: found rust-lldb: found $ cargo miri --version error: the 'miri' component which provides the command 'cargo-miri' is not available for the '1.99.1-aarch64-unknown-linux-gnu' toolchain
Your first project walks through Cargo’s own output, and Project structure covers workspaces and features, the Cargo answer to CMake targets.
A reading path
Section titled “A reading path”- Rust at a glance: the syntax on one page.
- What Rust lacks: your C and C++ habits that fail to compile, side by side.
- Stack and heap: confirms the model you have.
- Ownership: the replacement for
free,deleteandstd::move. - Borrowing and Mutable borrows:
const T&,T&and the one rule between them. - Working with the Borrow Checker: read this when the compiler says no.
- Enums, Option and Errors: variants,
NULLand exceptions. - Traits and Generics: classes and templates, in their places.
- Common traits:
Drop,Clone,PartialEq, the things you wrote operators and destructors for. - Box, Rc and RefCell:
unique_ptr,shared_ptrand what has no C++ twin. - Modules: the replacement for headers.
- Threads, Shared state and Send and Sync: data races as compile errors.
unsafeand C and C++ interop: last, because you can stay away from both for a long while.
What you will miss
Section titled “What you will miss”These are limits you can show, not opinions.
- Overloading. Two functions with one name are E0428, and a call with the other argument list is E0061. Use different names, a trait, or a builder (No default or named arguments, no overloading).
error[E0428]: the name `area` is defined multiple times --> src/bin/miss_overload.rs:5:1 | 1 | fn area(side: f64) -> f64 { | ------------------------- previous definition of the value `area` here ... 5 | fn area(width: f64, height: f64) -> f64 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `area` redefined here | = note: `area` must be defined only once in the value namespace of this module error[E0061]: this function takes 1 argument but 2 arguments were supplied --> src/bin/miss_overload.rs:10:34 | 10 | println!("{} {}", area(2.0), area(2.0, 3.0)); | ^^^^ --- unexpected argument #2 of type `{float}` | note: function defined here --> src/bin/miss_overload.rs:1:4 | 1 | fn area(side: f64) -> f64 { | ^^^^ help: remove the extra argument | 10 - println!("{} {}", area(2.0), area(2.0, 3.0)); 10 + println!("{} {}", area(2.0), area(2.0)); | Some errors have detailed explanations: E0061, E0428.
- Specialisation. C++ lets you replace a template for one type. A Rust blanket
impl<T>and animplfori32overlap (E0119).
$ g++ -std=c++20 -Wall -Wextra cpp/miss_specialise.cpp -o miss_specialise && ./miss_specialise # C++ an integer, something $ cargo build --bin miss_specialise error[E0119]: conflicting implementations of trait `Describe` for type `i32` --> src/bin/miss_specialise.rs:11:1 | 5 | impl<T> Describe for T { | ---------------------- first implementation here ... 11 | impl Describe for i32 { | ^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `i32`
- Layout guarantees by default. The Reference says the default representation makes only the layout guarantees that soundness requires, so a shared struct needs
#[repr(C)]. - Allocators in the collections.
std::vectortakes an allocator as a template parameter;Vec::new_inis behind an unstable feature on 1.99.1 (E0658).
$ g++ -std=c++20 -Wall -Wextra cpp/miss_allocator.cpp -o miss_allocator && ./miss_allocator # C++ 1 $ cargo build --bin miss_allocator error[E0658]: use of unstable library feature `allocator_api` --> src/bin/miss_allocator.rs:4:25 | 4 | let mut v: Vec<i32, System> = Vec::new_in(System); | -----^^^^^^ | | | help: consider wrapping the inner types in tuple: `(i32, System)` | = note: see issue #32838 <https://github.com/rust-lang/rust/issues/32838> for more information error[E0658]: use of unstable library feature `allocator_api` --> src/bin/miss_allocator.rs:4:35 | 4 | let mut v: Vec<i32, System> = Vec::new_in(System); | ^^^^^^^^^^^ | = note: see issue #32838 <https://github.com/rust-lang/rust/issues/32838> for more information
- Correct code that the checker rejects. This function returns a borrow from one branch and inserts in the other. It has the same shape as problem case #3 in the RFC that introduced non-lexical lifetimes, and it is E0502 on 1.99.1. The
entryAPI is the usual rewrite.
error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable --> src/bin/miss_rejected.rs:7:5 | 3 | fn get_or_insert(map: &mut HashMap<u32, String>, key: u32) -> &String { | - let's call the lifetime of this reference `'1` 4 | if let Some(found) = map.get(&key) { | --- immutable borrow occurs here 5 | return found; | ----- returning this value requires that `*map` is borrowed for `'1` 6 | } 7 | map.insert(key, format!("value {key}")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
$ cargo run --bin miss_rejected_fixed
value 1
value 1
Try it yourself
Section titled “Try it yourself”- In
h04_push.rs, printfirstbeforev.push(4). It compiles and prints1, because a borrow lasts until its last use (A borrow ends at its last use). - Run the release build of habit 9 with
CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS=true. The release build now panics like the debug one (Cargo config). - Run habit 10 and print the exit status. A
mainthat returnsErrexits with 1, where the C++ run above aborted.
$ cargo run --bin h04_push # after moving the println! above v.push(4)
1
[1, 2, 3, 4]
checked: None
wrapping: 44
widened: 300
thread 'main' (1464) panicked at src/bin/h09_overflow.rs:8:27:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace$ cargo run -q --bin h10_result > /dev/null; echo "exit code $?"
Error: ParseIntError { kind: InvalidDigit }
exit code 1
Read more
Section titled “Read more”- The Rust Book, chapter 4: ownership, in the book’s order.
- The Rust Book, chapter 9: panics and
Result. - The Rust Reference: behavior considered undefined: the list of what
unsafecode must not do. - The Rustonomicon:
unsafeRust in depth. - GCC instrumentation options: the
-fsanitizeflags used on this page.
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.