Rust compared with C++ — Quick Five
I. Memory Safety
A. Rust Rust prevents undefined behavior and data races at compile time. It guarantees memory safety without a garbage collector.
- Ownership and borrowing — Rust uses ownership and borrowing to manage memory. Each value has a single owner that can mutate it. Borrows are read-only aliases of values.
- Move semantics — When a value is passed to a function or assigned to a variable, the ownership is moved. The original variable can no longer be used.
- Copy types — Certain types like integers are copyable, so ownership is not moved when passed to a function.
fn main() {
let mut x = 5; // x is owned by this variable
let y = x; // x is moved to y, x can no longer be used
let z = x; // Compiler error, x was moved above
}
fn add(x: i32) -> i32 { // x is a copy, ownership not moved
x + 1
}
let a = 5;
let b = add(a); // a is still usable, integers are copyable
- Automatic memory management — Memory is freed automatically when variables go out of scope. This prevents memory leaks.
- No null pointers — The NULL value does not exist in Rust, preventing null pointer dereferences.
- Lifetimes — The lifetime of references can be annotated to ensure they do not outlive the values they reference.
B. C++ does not guarantee memory safety and is prone to undefined behavior and data races.
- Manual memory management — The programmer must explicitly allocate and free memory with new/delete and malloc/free. This can lead to memory leaks if memory is not freed.
- Pointers can be null — Dereferencing a null pointer leads to undefined behavior.
- No concept of ownership or lifetimes — It is possible to have dangling pointers referencing deallocated memory, leading to undefined behavior.
II. Concurrency
A. Rust Rust provides memory safe concurrency through language features and libraries.
- Ownership prevents data races — The ownership and borrowing rules apply to concurrent code, preventing data races at compile time.
- Threads with channels and message passing — The std::thread library is used to spawn threads, and channels are used to pass messages between threads.
- Mutexes and Atomics for mutating shared data — The Mutex and Atomic types can be used to mutate shared data across threads.
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel(); // Create a channel
thread::spawn(move || {
let msg = "hi";
tx.send(msg).unwrap(); // Send a message through the channel
});
let received = rx.recv().unwrap(); // Receive the message
println!("Got: {}", received);
}
B. C++ also provides facilities for concurrency, but it is possible to introduce data races which lead to undefined behavior.
- lock/unlock — Mutual exclusion can be achieved with lock and unlock.
- std::mutex and std::atomic — The std::mutex and std::atomic types can be used to protect shared data from being accessed concurrently.
- Data races are memory unsafe — If multiple threads access the same data concurrently without synchronization, it leads to a data race which results in undefined behavior.
III. Traits and Generics
A. Rust uses traits and generics to abstract over types.
- Traits are interfaces — Traits define an interface that types can implement. This enables shared behavior between types.
- Generics
- functions: e.g. fn foo(x: T)
- structures: e.g. struct Foo
- Bounds
- Trait bounds: e.g. fn foo(x: T) — The T type must implement the Display trait.
- Lifetime bounds: e.g. fn foo<’a, T>(x: &’a T) — The lifetime of the reference x must outlive ‘a.
B. C++ has some facilities for abstraction and generics through templates.
- Templates — C++ templates are used to abstract over types and can be used for functions, classes, etc. They are more limited than Rust’s generics.
- Concepts (in C++20) — C++20 introduces concepts, which are similar to Rust’s trait bounds.
IV. Syntax
A. Rust has an expression-based syntax with some key differences from C++.
- fn for functions
- let for bindings
- if/else, for and while loops
- No classes, uses structs and enums
fn add(x: i32, y: i32) -> i32 {
x + y
}
let x = 5;
if x == 5 {
println!("x is five!");
}
B. C++ has a class-based object oriented syntax.
- void foo() for functions
- int x; for bindings
- if/else, for and while loops
- Classes and structs
- Class Foo { … }; to define a class.
V. Package Management
A. Rust Rust has Cargo, a built-in package manager and build tool.
- Cargo (built-in package manager and build tool)
B. C++ C++ has some third party package managers.
- Conan, vcpkg, etc (third party package managers)
VI. Compile-Time Checks
A. Rust Rust checks for errors at compile-time and prevents undefined behavior.
- Checks for errors at compile-time
- No undefined behavior
B. C++ C++ performs some checks at compile-time but still allows undefined behavior.
- Some checks at compile-time
- Undefined behavior can lead to unexpected runtime errors
VII. Learning Curve
A. Rust Rust has some unique features that take time to learn.
- Ownership and borrow checker require learning.
B. C++ is an enormous, complex language that is difficult to learn completely.
- Large language with many features spanning 40+ years.