Opt-Out vs Opt-In Safety
Sometimes you are writing performance critical code, and need to avoid the added overhead of Rust's safety checks. It's also worth noting that C++ has safe versions of many of the things that Rust protects against---but they are opt-in, rather than opt-out. Rust reverses the defaults: you have to opt-out of the safe path if you need to.
Example: Opting Out of Range Checking
If you are genuinely certain that a vector will always contain the element you are looking for, you can opt out of the range check. It's a relatively tiny performance change, but it can make a difference:
fn main() { let my_vec = vec![1, 2, 3, 4, 5]; let entry = unsafe { my_vec.get_unchecked(2) }; println!("{entry}"); }
Before you start using
unchecked
functions everywhere, do some profiling to make sure that this really is the bottleneck! Wherever you can, prefer safe code.
Notice that you had to use the unsafe
scope tag---otherwise the program won't compile. This is common to Rust: code that may violate the safety guarantees has to be wrapped in unsafe
. This acts as a flag for other developers to check that part of the code. There's nothing wrong with having some unsafe
code---if you use it responsibly and with appropriate care and attention. Pretty much every Rust program has unsafe
in it somewhere; the standard library is full of unsafe
code. Once you leave Rust's domain and call into the operating system, you are leaving Rust's area of control---so the exterior call is inherently "unsafe". That doesn't mean its bad, it just means that you have to be careful.
So what happens if we access a non-existent part of the vector?
fn main() { let my_vec = vec![1, 2, 3, 4, 5]; let entry = unsafe { my_vec.get_unchecked(5) }; println!("{entry}"); }
In my case, I see the output 0
. There isn't a zero in the vector. We're reading past the end of the vector, just like we did in C++. The unsafe
tag has let you do bypass Rust's memory safety guaranty.
Example of What Not to Do: Turn off safety and enjoy a data race
On the other end of the spectrum, the unsafe
tag really does let you do bad things. You can recreate the counter example with unsafe code and have exactly the same problem you started with:
The code for this is in
projects/part2/unsafe_threading
fn main() { static mut COUNTER: u32 = 0; std::thread::scope(|scope| { let t1 = scope.spawn(|| { for _ in 0 .. 1000000 { unsafe { COUNTER += 1; } } }); let t2 = scope.spawn(|| { for _ in 0 .. 1000000 { unsafe { COUNTER += 1; } } }); let _ = t1.join(); let _ = t2.join(); // let _ means "ignore" - we're ignoring the result type }); unsafe { println!("{COUNTER}"); } }
Please don't do this. This is just for illustration purposes!