Cpp Cheatsheet

Rust Fundamentals

Overview

  • In many ways, Rust is "Modern C++ best practices distilled into a new language"
    • Smart pointers & Move semantics by default - explicit copy construction
    • Everything is const by default, opt-out with mut
    • Value Semantics & Data-oriented programming over complex object graphs
  • Writing Safe Rust gets you the following benefits instantly:
    • References are checked to be valid while in use
    • running UBSAN, ASAN, THREADSAN and RAII analysis at compile time, without the performance penalty at runtime
    • all code you depend on is also analyzed under the same constraints by the compiler

Installation

  • Cargo is the package manager, not the compiler
    • Like e.g. CMake, Cargo manages the compiler (rustc) and linker for you
    • Uses the system linker
  • Cargo does not have a separate "configure" stage like CMake
    • It's possible to use rustc directly, but very rarely needed
  • rustc is the compiler
  • CodeLLDB is a reliable debugger setup
  • Rust comes with an auto-formatter (rustfmt) that should be used by default
    • More consistent & reliable than e.g. clang-format
    • Most code in the Rust ecosystem uses rustfmt style

Basic Types

Integers

  • No int type, use i32 instead
  • Use usize wherever you would use std::size_t in C++
  • Integers in Rust cannot be used as booleans
    • Use explicit if my_number != 0 instead of if (my_number)

Strings and arrays

  • char in Rust represents an actual Unicode Scalar Value (21-bit)
    • It cannot be used to represent plain "bytes" - use u8/i8 instead
    • For C/C++ FFI, use std::os::raw::c_char
  • Rust Strings and string slices
    • No nul-terminator! - Not compatible with C strings!
    • String is not Small String optimized, like in C++
      • Try a drop in replacement like smallstr instead
  • Array types ([T;n]) are closer to std::array than to C-style arrays
    • Length is always known, includes bounds checking, etc.

Miscellaneous

  • Rust's println! semantics for non-numerics follow those of sprintf, but with {}:
    • %-10s to format a left aligned string padded to minimum 10 spaces becomes {:<10}
    • %04 to pad a number with zeros up to a width of 4 becomes {:04}, etc.
  • Rust does not have user defined literals so you need a macro to make let duration = 5_milliseconds; work in Rust
  • Raw pointers do exist but are rarely used
    • References or smart pointers are preferred for added semantics and safety

Control Flow

Note that Rust does not split functions into declaration and corresponding definition. Usually a function is simply defined. Rust can hoist everything and does not use a text-based include system. There is no need to "forward-declare" anything.

The only way to "declare" a function is if the function is a foreign function. Foreign functions are declared in an extern "..." block (usually extern "C").

This looks very similar to a declaration of an extern function in C/C++:

#![allow(unused)]
fn main() {
extern "C" {
    fn cpp_function(value: i32) -> i32;
}
}

if statements

  • No ternary operator in Rust
    • cond ? a : b becomes if cond { a } else { b }

match vs switch

  • match can match arbitrary types, not just integers
  • No fall-through and no break in match statements
    • Use "hello" | "world" => { ... } to match multiple things to the same result
  • _ is equivalent to default:
  • match is an expression like if - evaluates to the value of the match arm

do-while in Rust

There is no do {} while(); loop in Rust. It can be approximated with loop.

do {
    do_thing();
} while(condition());

becomes

fn do_thing() {}
fn condition() -> bool { false }
loop {
    do_thing();
    if (!condition()) {
        break;
    }
}

For loops

This C++ loop

const auto list = {1,2,3,4};
for (const auto &value: list) {
    //...
}

is equivalent to this Rust loop

for value in &list {
    //...
}

Compound Types

Structs

No class type in Rust

  • Use struct instead
  • Only data members are declared inside the struct
  • Member functions are declared outside the struct itself
  • No inheritance, Rust uses composition and traits instead (foreshadowing 👻)

Construction

Construction in Rust is similar to aggregate initialization with designated initializers.

There are no constructors in Rust, use "static" member functions (Rust calls them "associated functions") instead to uphold invariants before construction.

Enums

A Rust enum is most similar to a std::variant, it can hold data in each of the variants.

  • By default, the compiler chooses an optimal layout
    • Representation can be chosen explicitly with an attribute, e.g. #[repr(u8)]
    • Enum values can only be of the declared variants - not any integer
    • Cannot use enums as flags directly - use libraries like bitflags

Ownership and Borrowing

Like C++, Rust fundamentally has three ways to pass ownership around:

  1. Moving the value
  2. Copying the data into a new value
  3. Handing out a reference to the value

The difference is in the defaults:

Taking something by-value in Rust by default means moving the value, not copying. In comparison, copying is usually explicit with .clone() and references are explicit with & and &mut.

The second important difference is that a move does not leave an object behind; when moving out of an object, the moved-from object is no longer accessible.

References Cheat Sheet

C++Rust
Shared Reference Declarationconst std::string &argarg: &String
or: arg: &str
Shared Reference Passingfoo(arg);foo(&arg);
Exclusive Reference Declarationstd::string &argarg: &mut String
Exclusive Reference Passingfoo(arg);foo(&mut arg);

Rules around References/Borrowing

Rust's references are similar to C++ references, but many rules/best-practices that C++ holds you responsible for are enforced at compile time:

  • The referenced object must outlive the reference
  • Only one exclusive (mutable) reference can exist at any given point in the program
  • There can only be an exclusive reference, if there are no shared (const) references

In C++, these were already good to adhere to, in Rust they are mandatory - a safe Rust program will not compile otherwise.

Copy trait

The Copy trait changes Rust's default semantics back.

If a type implements Copy, it does not use move semantics, but copy semantics for assignment, passing by value, etc. Copy types behave very closely to the C++ defaults, but without the ability to be moved (i.e. similar to pre-C++11).

Copy is usually only used for plain-old-data types that are cheap to copy.

RAII and Drop

Rust uses very similar RAII rules to C++. Instances that go out of scope are dropped (i.e. destructed).

The Drop trait acts like the destructor in C++, it can run code just before the instance is deleted.

Rust uses this to implement automatic clean up, similar to C++ (e.g. String, Vec, etc. also free their resources when they go out of scope).

Error Handling

Think of all functions as noexcept, unless they return a Result.

They may still panic!, thereby aborting the program, similar to a noexcept function that throws an exception. However, panic! should be used rarely and documented well in public API.

Collections

Rust/C++ equivalents of common collections

C++RustNotes
std::array<T, n>[T;n]
std::span<const T>&[T]
std::span<T>&mut [T]
std::vector<T>Vec<T>
std::string_view&strRust: UTF-8
const char *&'static strString literals only
Rust: UTF-8
std::stringStringRust: UTF-8
std::dequeVecDequeBest match, slightly different internals
std::unordered_map<K,V>std::collections::HashMap<K,V>
std::map<K,V>std::collections::BTreeMap<K,V>Best match, slightly different internals
std::unordered_set<T>std::collections::HashSet<T>
std::set<T>std::collections::BTreeSet<T>Best match, slightly different internals

Iterators

Iterators in Rust are self-contained. No need for an end iterator.

Most algorithms are implemented directly on the Iterator trait, not as separate functions.

So this C++ code:

auto numbers = std::vector{ 1, 2, 3 };

auto odd = std::find_if(
                numbers.begin(),
                numbers.end(),
                [](const auto& number) { return number % 2 == 0; });

if (odd != numbers.end()) {
    std::cout << *odd << std::endl;
}

becomes:

#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3];

let odd = numbers
            .iter()
            .find(|number| *number % 2 == 0);

if let Some(odd) = odd {
    println!("{odd}");
}
}

Imports and Modules

Rust modules work like C++ modules, not like #include files. They export certain symbols (i.e. types/functions) under a given name. They are not included as in-place text.

Modules are also important for scoping in Rust. Unlike namespaces, modules and all items in them have their own visibility (e.g. pub or not).

Inside a module, all items can access each other, but items are only accessible from outside the module if they are pub. You can think of everything inside the same module as being a friend (as in the C++ keyword friend) of everything else inside the same module.

e.g. this works in Rust:

#![allow(unused)]
fn main() {
mod config {
    // private struct inside the module
    struct Config {
        // with private members
        color_enabled: bool,
        unicode_supported: bool,
    }

    // non-member function can still access the members inside the same module
    fn is_color_enabled(config: &Config) -> bool {
        config.color_enabled
    }
}

// But this would not work, as it's outside of `mod config`:
// fn is_unicode_supported(config: &Config) -> bool {
//     config.is_unicode_supported
// }
}

Good Design Practices

Operator Overloading with Traits

Traits like PartialEq, PartialOrd, etc. are Rust's way of operator overloading. If a type implements the right trait, the corresponding operator (e.g. ==, <, etc.) is available for the type.

#[derive(...)], implements traits automatically (comparable to = default in C++). You can also implement them manually.

See the std::ops module for details.

Applied Rust

Methods and Traits

A note on terminology: Rust uses "methods" where C++ developers might say "member functions". Both concepts are similar, many Rust developers will know what is meant by "member function".

In Rust, "static member functions" are called "associated functions".

Method Receivers

Instead of member functions that are declared inside the class, Rust defines static/non-static methods inside impl T blocks:

Mappings from "member functions" to "methods"/"associated functions":

C++Rust
void my_fun() const;
or: void my_fun() const &;
fn my_fun(&self) {}
void my_fun();
or: void my_fun() &;
fn my_fun(&mut self) {}
Not a direct equivalent!
void my_fun() &&;
fn my_fun(self) {}Calling a self
method consumes
the value, it is
no longer available.
static void my_fun();fn my_fun() {}

Note: Instead of constructors, use associated functions that return -> Self.

Note: Methods can also be implemented on enum and union types, not just struct.

Name Resolution inside methods

Rust uses self instead of this. The type of self depends on the declaration, and is usually a reference, not a pointer.

Inside method you must self. to access other methods/members explicitly. Unlike C++, members/methods are not implicitly added to the scope.

So the C++ member function area:

struct Square {
    float width() const { return m_width; }

    float area() const {
        return width() * width();
    }

    float m_width;
}

becomes:

#![allow(unused)]
fn main() {
struct Square {
    width: f64
}

impl Square {
    fn width(&self) -> f64 { self.width }

    fn area(&self) -> f64 {
        // Note: self is a &Square, not a *const Square
        self.width() * self.width()
    }
}
}

Rust differentiates between self.width (the member) and self.width() (the method). The name resolution only searches for methods/functions if the item is called with () and members/variables in the other cases.

Takeaway: No need to prefix members with m_ or similar! Prefixing members is considered bad practice in Rust.

Advanced note: If a member is itself a callable function, force member resolution first, by enclosing the member access in parentheses:

(self.callable_member)();

Interfaces without Inheritance

Rust is not a purely object-oriented language - only some object-oriented concepts are supported. Specifically, Rust does not support inheritance!

Then how to build abstractions in Rust? Use composition and interfaces instead.

Compound types like Structs/enums take care of the composition part of the equation. Traits represent the interface part.

Traits as interfaces

Traits are Rust's way of declaring interfaces - they are (very roughly) comparable to (abstract) base classes without members.

Key differences:

  • Traits don't describe an "is a" relationship, but a "supports" relationship
    • e.g.: String "supports" Format
  • Implementing a trait do not change members/memory layout of the type
  • By default: No virtual dispatch
    • Rust prefers generics over dynamic dispatch
    • Dynamic dispatch is opt-in by the trait user with dyn
  • Traits can be implemented on any type, not just struct types
    • Even reference/pointer types like &SomeType, *const SomeType, etc.

Using Traits statically

Static trait dispatch is roughly equivalent to C++ templates with C++20 concepts. Think of impl Trait as a concept that matches any type that implements Trait.

"Monomorphisation" is Rust speak for "template instantiation".

Using Traits dynamically

Rust dyn vs. C++ virtual.

  • Both use vtables for dynamic dispatch
  • Key differences
    • dyn is specified at the usage site, not the trait implementation
    • dyn applies to the whole trait, not per-function
    • vtable is stored in the pointer itself (&dyn Trait), not the struct type
  • Takeaways
    • Rust prefers static dispatch
    • Typically faster at runtime - can inflate binary size
    • Rust allows mixing static & dynamic dispatch depending on the usage
  • dyn and destruction
    • dyn vtables in Rust automatically reference the correct Drop implementation
    • No need to worry about "virtual destructors"

Rust I/O Traits

Rust separates between buffered and unbuffered I/O.

Read/Write take care of the underlying unbuffered I/O. BufReader/BufWriter can wrap any type that implements Read/Write and themselves also implement Read/Write.

In a sense the Read/Write define a basic interface, similar to C++ std::istream/std::ostream that other types implement.

Generics

Generics are basically C++ templates, but without the confusing pitfalls and terrible error messages (or at least a lot fewer of them).

For those reasons they are used in Rust widely and preferred over dynamic dispatch with dyn.

Type Inference

The Rust compiler is a lot smarter about type inference than the C++ compiler. In many cases, explicit type annotations are not needed, which can sometimes seem like magic.

To demystify this, it's important to know that Rust type inference can work "backwards" and only needs the missing bit of information. E.g. Rust can detect the type on a return value by going backwards from where the type is used to where it is created. Anything that can be inferred automatically can be left out of the type declaration with _.

Example:

#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3, 4];

// No need to supply the item type of the `Vec`, leave out with `_`
let odds: Vec<_> = numbers
    .into_iter()
    .filter(|num| *num % 2 != 0)
    // `.collect()` on this iterator can return anything that implements `FromIterator<i32>`.
    // Rust determines to use `Vec<i32>` because the result is assigned to `odds`,
    // which must be a `Vec` of something.
    // Because only `Vec<i32>` implements `FromIterator<i32>`, it must be `Vec<i32>`.
    .collect();

assert_eq!(odds, vec![1,3]);
}

Adding Bounds

Rust trait bounds are roughly comparable to C++20 concepts. They require a type to implement the given traits to be used with the generic.

Important difference: The generic can only access functions/items that are declared in the bounds! The compiler checks the generic in isolation, not for each specialization individually!

C++ template type-checking:

  1. Check any concepts
  2. Insert the type into the template
  3. Type check (may still fail)

Rust generic type check:

  1. Type check the generic with the given bounds
  2. Check that the concrete type actually implements the bounds
  3. Insert the type into the template (can no longer fail)

This re-ordering means error messages are much cleaner, as the generic itself is checked for correctness, not every concrete instantiation.

Lifetimes

In C++ documentation you will often find important notes about invalidation of references/iterators, etc.

An example from cppreference.com:

std::vector<T,Allocator>::clear

void clear();

Erases all elements from the container. After this call, size() returns zero.

Invalidates any references, pointers, and iterators referring to contained elements. Any past-the-end iterators are also invalidated

Catch the note about invalidation of references/pointers and iterators?

This documentation isn't just a minor detail, it is vitally important to the correctness of the program! Any program that uses said references/pointers or iterators after calling clear() is immediately undefined behavior! In C/C++ ensuring this does not happen at compile time, is almost impossible 😢.

Lifetimes are here to help! They give Rust a way to express these relationships between references and whatever they reference as a language construct. So instead of documenting these important relations as text and hoping people read the documentation, lifetimes allow Rust to enforce them at compile time!

You will probably recognize many of the issues that the Rust compiler prevents as common pitfalls in C/C++.

Cargo Workspaces

In Rust, the smallest compilation unit is a whole crate, not just one source file.

At the time of writing (05/2025), the Rust compiler is largely single-threaded. Adding more crates to your build therefore tends to speed up compilation by allowing Cargo to compile multiple compilation units in parallel.

In general, splitting your project into multiple crates is very common and good practice in Rust. Package management is considerably easier compared to C++, do not be afraid to split your project into multiple smaller packages.

Heap Allocation (Box and Rc)

Heap allocation in Rust is almost always done via smart pointers.

Rust smart pointers are very similar to the C++ equivalents, but cannot be null! If you need a nullable smart pointer use them together with Option<T>, e.g. Option<Box<T>>.

C++Rust
std::unique_ptr<T>Box<T>
std::shared_ptr<T>std::sync::Arc<T>
(std::rc::Rc<T> if single-threaded)
std::weak_ptr<T>std::sync::Weak<T>
(std::rc::Weak<T> if single-threaded)

Arc and sync::Weak use atomic reference counting (like std::shared_ptr/std::weak_ptr). Rc and rc::Weak are faster, but limited to a single thread.

Shared Mutability (Cell, RefCell, OnceCell)

With mutability, Rust follows broadly the same practices that you should follow in C++, but Rust actually enforces the rules.

Two mutable references can never exist at the same time! It is impossible in safe Rust, and is UB in unsafe Rust.

Shared (or "interior") mutability usually ensures at runtime that only a single mutable reference can exist at the same time, even if it's not possible to prove at compile time.

Thread Safety (Send/Sync, Arc, Mutex)

Mutexes

In C++, you have to manually ensure that you lock the right mutex for the right data. A Rust Mutex<T> can be thought of as a std::mutex together with some arbitrary data of type T. The data T is owned and protected by the mutex.

Mutex::lock returns a MutexGuard, which is similar to a std::lock_guard. Note that it does not include a deadlock avoidance algorithm like std::scoped_lock!

MutexGuard dereferences (mutably) to the inner T data, thereby granting mutable access.

Arc vs. shared_ptr

Arc is Rust's std::shared_ptr and also uses atomics for thread-safe reference counting.

In C++ you must take care to never mutate a shared_ptr from multiple threads, as the shared_ptr itself is not thread-safe, only the internal reference counting. Arc is implemented the same way, but as it is impossible to gain two mutable references to the same Arc in Rust, you do not need to worry about this.

Atomics

Atomics in Rust are pretty much the same as in C++. However, they are not generic types, but multiple concrete types.

So instead of std::atomic<int>, use std::sync::atomic::AtomicI32, etc.

Rust atomics also include some helper methods for easier compare-exchange loops. For example: AtomicI32::fetch_update.

Closures and the Fn/FnOnce/FnMut traits

Fn/FnMut/FnOnce are traits, not concrete types. They are Rusts way of expressing an operator() implementation.

C++Rust
T operator()(...) constFn(...) -> TNeeds & to call
T operator()(...)FnMut(...) -> TNeeds &mut to call
Not a direct equivalent!
T operator()(...) &&
FnOnce(...) -> TNeeds ownership to call

To store any callable type, similar to std::function<(...)>, use Box<dyn Fn(...)> or one of the other traits.

Note: Box<dyn Fn...> is rarely needed - prefer using generics with Fn/FnMut/FnOnce trait bounds.

Capturing data in closures

Closures are the Rust equivalent of C++ lambdas. Unlike C++, Rust does not have an explicit capture list. By default, every outside variable is captured by reference.

Therefore |arg| { ... } is the Rust equivalent of [&](auto arg) { ... }. To capture everything by-value (e.g. by move), add the move keyword (e.g. move || { ... }).

If you need to specify explicitly which types to capture by-value/by-copy/by-reference, use move together with an added scope.

e.g. this C++ capture list:

auto by_move = std::string("Hello Move");
auto by_copy = std::string("Hello Copy");
auto by_reference = std::string("Hello Reference");

auto lambda = [by_copy, by_move=move(by_move), &by_reference]() {
    // ...
};

becomes:

#![allow(unused)]
fn main() {
let by_value = "Hello Move".to_owned();
let by_copy = "Hello Copy".to_owned();
let by_reference = "Hello Reference".to_owned();

// create a scope for the closure
let closure = {
    let by_reference = &by_reference; // shadow with a reference
    // let by_reference = &mut by_reference; // or mutable reference
    let by_copy = by_copy.clone(); // or explicit clone

    // The closure now captures the references by move, not the values themselves
    move || {
        // ...
    }
};
}

Spawning Threads and Scoped Threads

Advanced Rust

Advanced Strings

String is Rust's equivalent to std::string. &str is closest to a std::string_view.

Key differences:

  • String&&str are guaranteed to be valid UTF-8
    • Don't assume ASCII characters inside
    • Iterating over the bytes is usually not what you want!
    • Use chars() to iterate over the characters
  • String&&str are not nul-terminated!
    • Do not use String/&str data as const char* when calling C functions!
    • Use CString/CStr instead, they are nul-terminated
  • char in Rust is not one byte!
    • It actually represents a "Unicode character"
    • Specifically one "Unicode scalar value"

Building Robust Programs with Kani

Dealing with Unwrap

Debugging Rust

Rust emits a very similar binaries and debug information to C/C++ binaries.

=> Most C/C++ tooling will work for Rust debugging/profiling to some extent

Tools usually have to add Rust support for:

  • Syntax highlighting Rust code
  • Symbol demangling
  • Layout information for instance introspection
  • Pretty-printing for common types (e.g. String)

Some common C/C++ tools that support Rust out-of-the-box:

Deconstructing Send, Arc, and Mutex

Dependency Management with Cargo

Cargo uses Toml files to describe packages. These files are not "executed" like CMake files, they provide static data.

If you need to "compute" something at build time, Cargo allows you to run Rust scripts that can configure certain parts of the build process (e.g. to discover C libraries to link, etc.). No need to learn a second programming language for your build system.

Deref Coercions

Deref is comparable to a mix of operator-> and inheritance.

The way it works is basically like an operator-> overload, but instead of overloading the -> operator, you can directly overload method and field access (.). Deref and DerefMut are therefore usually implemented on smart pointer types (e.g. Box, Rc, Arc).

This is a convenient way to "inherit" behavior by using composition. Instead of inheriting from a type T, you store an instance of T inside the struct and add Deref and DerefMut implementations that dereference to the T value. From the outside it almost looks like the outer types supports all the same operations as T, similar to inheritance in C++.

A prominent example of this is String, which "inherits" all methods from str by dereferencing to it.

Design Patterns

Notes on Cloning

These Design guidelines recommend using .clone() in many cases. You may worry that this is slow, especially compared to C++.

However, remember that in C++ copy-construction is often the default! For example, think of all constructors that take const std::string& - these usually end up copying the whole string!

So even if you use .clone() liberally in Rust, your program will likely still clone less often than a similar C++ program! In Rust move-semantics are the default, if you take a String by-value, it does not incur a copy operation.

Takeaway: Don't be afraid of .clone(), C++ clones all the time anyway, your Rust code will probably still end up cloning less often that C++.

From<> and Into<>

Like with .clone(), Rust is explicit whenever a conversion occurs.

A From<> implementation is similar to an explicit conversion constructor in C++.

Documentation

Rustdoc is the default in Rust, please do use it.

Rustdoc uses Markdown for documentation. Unlike Doxygen, documentation is largely free-form and does not use "tags" like @param/@return/etc.

This style of documentation is less repetitive - the function name and signature should already be descriptive. Use the documentation to provide important context, not just repeat the list of arguments.

Drop, Panic and Abort

Rusts concept of RAII is very similar to C++ and intuition about both is largely the same. A resource is initialized when it is acquired and cleaned up when it is dropped (Rust terminology for "destructed"). The Drop trait is akin to the C++ destructor.

Like in C++, the members inside a struct/enum run their drop function after the drop function of the struct/enum itself.

Because of the way Rust handles dynamic dispatch, there is no such thing as a virtual function. Destructors do not have to be virtual and will work correctly with dynamic dispatch out of the box.

Panic vs. Exceptions

Even though a panic internally works similarly to a C++ exception, do not use panics for recoverable errors!

Think of panic as something that is so critical that aborting the program immediately is a valid response to the error (even if it may unwind in reality).

Dynamic Dispatch

Trait Objects are the closest thing Rust has to "virtual inheritance".

When used via a trait object, the trait is comparable to an abstract base class. Any type that implements the trait "inherits" from the "abstract base class".

Dynamic Dispatch via a Trait object differs to inheritance in where the vtable is referenced. In C++, the vtable is referenced inside the struct/class instance. In a trait object, the vtable is referenced inside the (smart) pointer to the struct/enum.

For this reason the pointer itself is the trait object, not the struct that implements the trait!

This also means a dyn pointer/reference is actually two pointers:

  1. Pointer to the data
  2. Pointer to the vtable
#![allow(unused)]
fn main() {
use std::{fmt::Display, mem::size_of};

assert_eq!(size_of::<&dyn Display>(),       2 * size_of::<&String>());
assert_eq!(size_of::<*const dyn Display>(), 2 * size_of::<*const String>());
assert_eq!(size_of::<Box<dyn Display>>(),   2 * size_of::<Box<String>>());
}

Because dynamic dispatch can only happen via a trait object, the compiler always knows when to use dynamic or static dispatch. In Rust, it is therefore not necessary to specify something like "virtual" on each function, all functions can be used with dynamic dispatch, as long as the trait is dyn-compatible. If the type is not dyn, Rust uses static dispatch automatically!

Macros

Disclaimer: Rust macros are way saner than C preprocessor macros!

Some important differences:

  • Are always explicitly invoked with my_macro! or #[my_macro]
    • Cannot accidentally be invoked 🥳
  • Don't just do text replacement, but operate on "tokens"
    • Closer, safer integration with the compiler
    • Operate somewhat like "compiler plugins/extensions"

For these reasons, Rust macros are a lot saner and easier to use. Then can and will still rewrite your code, so they should still be used sparingly.

Property Testing

Rust Projects Build Time

At the time of writing (May 2025), the Rust compiler is still largely single-threaded.

In Rust, the compilation unit is a whole crate, not a single file! To achieve parallelization, Cargo can schedule multiple crates to be compiled at the same time, as long as they are independent of each other.

Takeaway: Do not be afraid to split your project into more crates (and therefore compilation units) - this can improve compile time.

Send and Sync

Send and Sync are like lifetimes in the sense that they allow Rust to encode and enforce properties of types that have always existed, but could only be documented in plain text, not in the language itself (at least in C++).

Serde

Testing

The stdlib

Note that in Rust Zero-sized types are actually 0 bytes in size, they do not change the size or alignment of your type. In C/C++, even an empty struct has a non-zero size.

Using Cargo

Almost always you will use Cargo to work on Rust projects.

It is a front for "all things Rust" and will delegate many tasks to other tools, like the Rust compiler (rustc), clippy, etc. The Rust ecosystem is far more integrated around Cargo than the C++ ecosystem is around any build system or even compiler.

Cargo's build system is intentionally more limited than e.g. CMake. It focuses on doing one thing and doing it well: Compile Rust code and manage Rust dependencies. This has the advantage that almost all crates adhere to what Cargo expects and are therefore easy to understand and include in your project.

Using Types to encode State

Rust and Web Assembly

WASM