Rust compared with C++ — Quick Five

Amay B
CoderHack.com
Published in
4 min readSep 18, 2023
Photo by Tracy Adams on Unsplash

I. Memory Safety

A. Rust Rust prevents undefined behavior and data races at compile time. It guarantees memory safety without a garbage collector.

  1. 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
  1. Automatic memory management — Memory is freed automatically when variables go out of scope. This prevents memory leaks.
  2. No null pointers — The NULL value does not exist in Rust, preventing null pointer dereferences.
  3. 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.

  1. 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.
  2. Pointers can be null — Dereferencing a null pointer leads to undefined behavior.
  3. 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.

  1. Ownership prevents data races — The ownership and borrowing rules apply to concurrent code, preventing data races at compile time.
  2. Threads with channels and message passing — The std::thread library is used to spawn threads, and channels are used to pass messages between threads.
  3. 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.

  1. lock/unlock — Mutual exclusion can be achieved with lock and unlock.
  2. std::mutex and std::atomic — The std::mutex and std::atomic types can be used to protect shared data from being accessed concurrently.
  3. 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.

  1. Traits are interfaces — Traits define an interface that types can implement. This enables shared behavior between types.
  2. Generics
  3. functions: e.g. fn foo(x: T)
  4. structures: e.g. struct Foo
  5. Bounds
  6. Trait bounds: e.g. fn foo(x: T) — The T type must implement the Display trait.
  7. 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.

  1. 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.
  2. 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.

--

--