C++ vs Rust

WARNING: Terrible explainations of Rust, by someone who doesn't really know it, can be found bellow. Additionally, this is not meant as a full guide to the Rust language, but instead an extremely terse jumping off point for "I know how to do this in C++, but what's the Rust equivalent again?" style questions, and nonobvious differences from C++ that may trip you up comming from C++.

Reading List

The Rust Programming Language – Rust 101
The Rustonomicon – Rust for the person who no longer values their sanity.
What Not to Do in Rust – Touches on someone's mindset shifts when going from C++ -> Rust
kyren from Chucklefish in /r/rust – Talks about their experiences with rust gamedev
Notes on Type Layouts and ABIs in Rust
Rust Case Study: Chucklefish Taps Rust to Bring Safe Concurrency to Video Games

Watching List

Using Rust For Game Development – RustConf 2018 Closing KeyNote

Table of Contents

Common Errors
Type Equivalents[see also Notes on Type Layouts ...]
Syntax Equivalents
The Obvious
The Less Obvious
Interior Mutability[see also The Rust Programming Language]
Dynamically Sized Types[see also The Rustonomicon]
Virtual Dispatch
Function Overloading
Are we MaulingMonkey yet?

Common Errors

LNK1112: module machine type 'x86' conflicts with target machine type 'x64'

Use a regular cmd.exe context instead of a Developer Command Prompt or similar. Alternatively, pair vcvars32.bat or vcvarsall.bat x86 with a 32-bit target, with cargo build --target=i686-pc-windows-msvc. This may require you to rustup target install i686-pc-windows-msvc the appropriate target.

Because rustc.exe doesn't pass /MACHINE:... to link.exe, the architecture used depends on the link.exe invoked. Running vcvars___.bat sets %VCINSTALLDIR%, and causes rustc to use whatever link.exe is in %PATH% (e.g. ...\Hostx86\x86\link.exe), instead of allowing rustc to find one that matches the target architecture (e.g. ...\HostX64\%varies%\link.exe).

Type Equivalents

See also Notes on Type Layouts and ABIs in Rust.

This is an intentionally incomplete list with exemplar types and some overlap. Also note that for FFI, the Rust types sometimes impose additional restrictions over the equivalent C types. Bool must be 0/1 (not merely truthy), non-listed enum values have undefined behavior (maybe use a u?? instead at the FFI layer), etc.

Rust C++
boolbool, assuming sizeof(bool)==1
charchar32_t, "unicode scalar value"
u8char as used for typical byte buffers
u8, u16, u32, u64, u128 uint8_t, uint16_t, uint32_t, uint64_t, __uint128 [bug!]
i8, i16, i32, i64, i128 int8_t, int16_t, int32_t, int64_t, __int128 [bug!]
f32, f64 float, double, on most implementations
usize, isize uintptr_t (~size_t), intptr_t (~ptrdiff_t),
() – read as "unit"void
! – read as "never"[[noreturn]] void ?
&Struct – "(shareable) reference"const Struct &
&DynamicallySizedTypestd::tuple<const DynamicallySizedType *, size_t>
&dyn Traitstd::tuple<const TraitVtable *, const Struct &>
&Trait – Pre 1.27 versionstd::tuple<const TraitVtable *, const Struct &>
&mut T – read as "mutable reference"Like !mut, but exclusive access and writable
 
std::cell::Cell<T>mutable keyword + compile time checking
std::cell::RefCell<T>mutable keyword + run time checking
 
std::boxed::Box<T>unique_ptr<const T>, !null, value semantics
std::rc::Rc<T>shared_ptr<const T>, !null, single threaded, value semantics
std::rc::Weak<T>weak_ptr<const T>, single threaded
std::sync::Arc<T>shared_ptr<const T>, !null, atomic refcount, value semantics
std::sync::Weak<T>weak_ptr<const T>, atomic refcounts
std::option::Option<Box<T>>unique_ptr<const T>
std::option::Option<Arc<T>>shared_ptr<const T>
Option<Box<Cell<T>>>unique_ptr<T>
Option<Arc<Cell<T>>>shared_ptr<T>
Weak<Cell<T>>weak_ptr<T> – Weak is nullable, so no Option necessary
RefCell<Weak<Self>> [example]enable_shared_from_this
 
std::sync::atomic::AtomicBoolstd::atomic<bool>
std::sync::atomic::AtomicUsizestd::atomic<size_t>
std::sync::Mutex<Struct>std::atomic<Struct>, note C++ may not be atomic!
std::sync::Mutex<Struct>std::mutex m; Struct s GUARDED_BY(m);
 
std::string::Stringstd::string, except guaranteed UTF8
&strstd::string_view, except guaranteed UTF8
std::ffi::CStringstd::string
std::ffi::CStrstd::string_view
std::ffi::OsString~std::wstring on windows, but converted to WTF8(!)
std::ffi::OsStr~std::wstring_view, but wchar_t* isn't WTF8
 
std::vec::Vec<T>std::vector<T>
[T; 12]std::fixed_array<T, 12> , T[12]
&[T]std::array_view<T> , gsl::span<T>
std::collections::VecDeque<T>std::deque<T>
std::collections::LinkedList<T>std::list<T>
std::collections::BTreeSet<T>std::set<T>
std::collections::BTreeMap<K, V>std::map<K, V>
std::collections::HashSet<T>std::unordered_set<T>
std::collections::HashMap<K, V>std::unordered_map<K, V>
 
(A, B)std::tuple<A, B>
struct T(A, B);struct T : std::tuple<A, B> {};
struct T { a: A, b: B, }struct T { A a; B b; };
enum typesstd::variant + names and language support
enum typesC++ enums (although use #[repr(C)] for FFI)
type Foo = u64;typedef uint64_t Foo;
 
Exact Rust FFI TypeC++
()void (return type)
![[noreturn]] void (return type) ?
std::os::raw::c_void*void*
...::raw::c_float, c_double float, double
...::raw::c_char, c_schar, c_uchar char, signed char, unsigned char
...::raw::c_ushort, c_short unsigned short, short
...::raw::c_uint, c_int unsigned int, int
...::raw::c_ulong, c_long unsigned long, long
...::raw::c_ulonglong, c_longlong unsigned long long, long long

Syntax Equivalents

There are several unlisted differences in the exact behavior of C++ vs Rust code bellow - type inference being more powerful and extending beyond the current statement in Rust for example.

Rust C++
assert!(condition); assert(condition);
print!("Hello {}\n", world); printf("Hello %s\n", world);
 
if a == b { ... } if (a == b) { ... }
else if a == b { ... } else if (a == b) { ... }
else { ... } else { ... }
 
let foo = 42; const auto foo = 42;
let mut foo = 42; auto foo = 42;
let mut foo : i32 = 42; int32_t foo = 42;
let mut (x,y) = some_tuple; float x,y; std::tie(x,y) = some_tuple;
let some_tuple = (1,2); const auto some_tuple=std::make_tuple(1,2);
let a : [i32; 3] = [1, 2, 3]; int32_t a[3] = { 1, 2, 3 };
let v : Vec<i32> = vec![1, 2, 3]; std::vector<int32_t> v = { 1, 2, 3 };
 
for i in v { ... } for (auto i : v) { ... }
for i in 1..9 { ... } for (size_t i = 1; i < 9; ++i) { ... }
for i in 1..=9 { ... } for (size_t i = 1; i <= 9; ++i) { ... }
while i < 9 { ... } while (i < 9) { ... }
 
loop { while (true) {
    break;     break;
    continue;     continue;
} }
'label: loop { while (true) {
    break 'label;     goto label_break;
    continue 'label;     goto label_continue;
} label_continue: } label_break:
 
other_var as f32 (float)other_var
 
match var { ... } switch (var) { ... }
42 => foo(), case 42: foo(); break;
42 => { foo(); bar(); }, case 42: foo(); bar(); break;
_ => ..., default: ... break;
 
mod foo { ... } namespace foo { ... }
use foo as bar; using namespace bar = foo;
use foo::*; using namespace foo;
use foo::some_func; using foo::some_func;
 
/// Doc comment // \brief Doc comment
fn add (a: i32, b: i32) -> i32 { int32_t add (int32_t a, int32_t b) {
    return a + b;     return a + b;
    a + b // Final statement, no semicolon     return a + b; // A regular comment
} }

The Obvious

The Less Obvious

Interior Mutability

See also The Rust PL 2nd Ed. on Interior Mutability.

Like in C++, the constness of the outer object can affect access to it's members. For example, given const std::vector<int> & v, you cannot modify the elements of the vector with v[0] = 42; - this will fail to compile. Given a const gsl::span<int> & av, however, av[0] = 42; will compile just fine. Rust calls this second case "interior mutability".

Another example: In C++, given const some_struct & s, you cannot modify members with s.some_field = 42; unless some_field used the mutable keyword. Similarly, in Rust, given s : &SomeStruct, you cannot modify members with s.some_field = 42; unless some_field was wrapped in Cell or another type that provides "interior mutability".

Because immutability is tied to the ability to share a value in Rust, "interior mutability" is used in some places that may be suprising to a C++ programmer. For example, you can modify an "immutable" Rust std::sync::atomic::AtomicUsize, even though you wouldn't be able to modify a C++ const std::atomic<size_t>, because AtomicUsize has been given "interior mutability". Without interior mutability, it'd be impossible to share a AtomicUsize between multiple threads while allowing write access - defeating the entire purpouse of having atomic types in the first place.

Some of the types in Rust providing interior mutability - generally, you only need one at a time:

Rust Type Description
std::cell::Cell<T> Exists specifically to give you Interior Mutability for "Free". This type works by giving you methods like get(), and set(value), and enforcing at compile time that you never hold onto a &T or &mut T (unless you have an exclusive mutable reference to the whole Cell at once.) Cannot be shared between threads. T must be Copy able.
std::cell::RefCell<T> Exists specifically to give you Interior Mutability. Unlike Cell<T>, this allows you to get a &T or &mut T from an immutable instance of RefCell<T>, and instead enforces at run time that you'll never have a &mut T and any other references at the same time. Methods like borrow() can panic, but non-panicing options like try_borrow() are also available. Cannot be shared between threads.
std::cell::UnsafeCell<T> This exists, but you'll need to use unsafe code and raw pointers. Best of luck!
std::sync::Mutex<T> Similar to RefCell<T>, but thread safe, and so you lock() it instead of borrow()ing it. Doing so multiple times from the same thread is less specified: It may deadlock, or it may panic. It may also panic if another thread paniced while holding onto the mutex.
std::sync::RwLock<T> Similar to Mutex<T>, only allowing multiple readers simultaniously.
std::sync::AtomicUsize Atomic types also implement interior mutability so they can be shared between multiple threads.

Dynamically Sized Types

See also The Rustonomicon on Dynamically Sized Types.

In C++, all types have a single size, known at compile time. Structs with a final array of size 1 (or 0, if you're comfortable relying on compiler extensions) are often (ab)used to fake "Dynamically Sized Types" (or DSTs), where more memory is allocated than the C++ type alone needs, so the array can be (ab)used to index multiple elements. You would typically store the size in the type itself. For example:

struct Str {
    size_t length;
    char   characters[0];
};

static inline Str * create (const char * c_string) {
    size_t len = strlen(c_string);
    Str * s = (Str *)malloc(sizeof(Str) + len);
    memcpy(s->characters, c_string, len);
    return s;
}

In Rust, Dynamically Sized Types are natively supported by the language, although they have several limitations. We could write:

#[repr(C)]
struct MyStr {
    length:     usize, // You wouldn't actually do this - keep reading!
    characters: [u8],  // [u8] is also a DST - an array of an unknown number of elements.
                       // We can't use DSTs as fields, except as the last field like here.
};

References to DSTs (like &MyStr or &[u8]) in Rust are larger than regular references (like &NormalStruct or &i32). References to DSTs know the dynamic size of the type they're referencing, not just the address, so in Rust we typically wouldn't store the length inside the struct as well (it's redundant!). This also lets us do things like have multiple `&[u8]`s referencing different subsets of the same data.

The two most common DSTs you'll run into are str and [T], which is why you'll almost always seem them dealt with by reference (e.g. &str and &[T]) – you can't use them as standard values on the stack.

Virtual Dispatch

Rust structs don't contain vtable pointers. Instead, the reference or pointer to a trait object is a "fat pointer" - in other words, &dyn SomeTrait conceptually maps to two C++ pointers: One to the struct being passed in, and one to the trait vtable for that type.

Function Overloading

Rust doesn't have function overloading, but it has several tricks which can make it look like it does:

Are we MaulingMonkey yet?

Maybe for gamedev middleware or other greenfield projects.
No for agitating to replace existing C++ in gamedev codebases.
See Are we game yet? and Are we web yet? for more options.

Area Status Notes
HTTPS Good Rocket.rs is pretty neat and acceptable for the kinds of basic REST APIs I like to make. I haven't properly evaluated database and other web tech options for suitability for others however. AWWY?
ECS Good specs is neato and I should use it. No entity archtype optimizations yet, but I could always roll my own if I need that. See also the Using Rust For Game Development (RustConf 2018 Closing KeyNote) which talks about specs.
Debugging OK I've actually contributed patches to help improve things! Vec and other relatively basic types have .natvis visualizers which MSVC-linked rust executables will automatically embed into your .pdbs, allowing Visual Studio to debug your executables with ~0 configuration. Enums and more complicated types still need some work. Things like source code breakpoints work fine. The standard Visual Studio Code C/C++ Extension can also debug and launch Rust executables. GDB support also exists.
VS Code OK Use ms-vscode.cpptools for debugging. I've used kalitaalexey.vscode-rust for intellisense/building, although it can be tempermental to set up. I'm also going to give rust-lang.rust another shot.
Consoles OK

From the Rust Chucklefish Whitepaper (emphasis mine):

"Even after taking into account the ten days needed to customize Rust for the Xbox, PS4, and Nintendo Switch consoles, Chucklefish saved time previously spent debugging cross-platform idiosyncrasies."
That's the platforms I care about, and in a reasonable timeframe. Ten days is nothing compared to the time I've sunk into improving interop and debugging for various scripting languages. I expect the C style FFI will work out of the box, as will the debugger (already works on Windows/Linux MSVC, GDB, LDB.) The rust stdlib on the other hand might be a different matter.
Graphics Workable? bgfx-rs needs a maintainer and to be finished. There seems like a good assortment of other graphics libs though.
C++ FFI Workable

We've got several options.

Manual C ABI FFI. Fundamentally lame, but Rust's version is as workable as any other I've encountered. For fancy stuff like COM interop, beware of unslain dragons.

cbindgen (export Rust to C/C++): Lets you generate C/C++ headers for whatever C style ABI you expose from rust. C style ABIs only, with many unhandled edge cases (generates unusable headers for tuples, won't export constants using type aliases, etc.), but usable. Can be intergrated into your build.rs scripts with relative ease. You'll likely need to hand-write some interop types for strings, references, slices, etc.

rust-ffi (export Rust to C/C++): Same author as cbindgen, maybe better (looks to handle more edge cases). I need to try it out some.

bindgen (export C/C++ to Rust): Still haven't given this a proper go. Used by bgfx-rs AFAIK. Relatively limited when it comes to C++, but usable.

Passing Rust's unterminated string slices to pure C interfaces adds friction/allocation (well discussed by imgui-rs#7), although it's not a problem if you can pass ptr+length and simply convert to std::string_view s or similar on the C/C++ side of things. Similarly, Rust's insistence that &str must be a known-length UTF8 slice for defined behavior adds error checking/friction when passing strings back to Rust, although the standard library has * const c_char and CStr which you may be able to use instead. It's also worth noting that &str and &[T] aren't #[repr(C)] so you'll need to write intermediate types that are.

Visual Studio Poor Rust for Visual Studio Code is workable with enough configuration, and IntellJ Rust seems pretty good, but Visual Studio integration is an absolute must for professional gamedev and Visual Rust is not up to snuff, even on my branch of it.

— MaulingMonkey