More Rust compared to C++

After getting in touch with Rust, the next thing I was looking at were pointers and the concept of ownership and borrowing in Rust.

Pointers

Rust has a lot of different pointer types, reminding me of the C++ references, raw and smart pointers like std::unique_ptr, std::shared_ptr. For a nice comparison see here.

References

One of the first pointer type you will meet in Rust is the reference type.

fn main() {
    let mut x=1;
    println!("x = {}", x);
    add_one(&mut x);
    println!("x = {}", x);
}

fn add_one(num: &mut i32) {
    * num += 1;
}

In C++ this would look something like this:

#include <cstdio>

void add_one(int& num) {
    num += 1;
}

int main() {
    int x=1;
    printf("x = %i\n", x);
    add_one(x);
    printf("x = %i\n", x);
}

What is think is nice in Rust is:

  • It's explicit that x will be modified (&mut x)
  • It's guaranteed that x is neither dangling nor 0

In C++ references have value syntax. To avoid that I prefer the style of using pointers for mutable and only const reference for immutable data.

#include <cstdio>

void add_one(int* num) {
    *num += 1;
}

int main() {
    int x=1;
    printf("x = %i\n", x);
    add_one(&x);
    printf("x = %i\n", x);
}

This way the syntax looks much more like the Rust example, but you can pass 0:

    add_one(0);
-> Segmentation fault (core dumped)

Trying the same with C++ references yields a compile error, because a non const reference can't bind to a temporary (rvalue)

reference.cpp: In function 'int main()':
reference.cpp:12:11: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
  add_one(0);
           ^
reference.cpp:3:6: note: in passing argument 1 of 'void add_one(int&)'
 void add_one(int& num) {
      ^

Surprising to me, doing the same within Rust works

fn main() {
    add_one(&mut 0);
}

fn add_one(num: &mut i32) {
    print!("adding one to {}", num);
    * num += 1;
    println!(" -> {}", num);
}

Output: adding one to 0 -> 1. Apparently one can bind mutable references to temporaries in Rust.

Heap pointers

For managing data on the heap Rust has the Box<T> type

fn main() {
    let x = Box::new(5);
    println!("{}", * x);
}

Output: 5

The equivalent in C++ would be

#include <memory>
#include <cstdio>

int main() {
    auto x = std::make_unique<int>(5);
    printf("%i\n", *x);
}

Note that the compiler takes care of freeing the used resources as soon as x goes out of scope. In the C++ world this is called RAII.

Ownership, Borrowing and Lifetime

The Box type is an owning type. Meaning that it owns the resources it is pointing to. The same is true for std::unique_ptr.

When passing a Box type to a function the ownership transfers to this function:

fn add_one(mut num: Box<i32> ) {
    * num += 1;
}

fn main() {
    let x = Box::new(5);
    println!("{}", * x);
    add_one(x);
    println!("{}", * x);
}

This will result in a compile error:

boxed.rs:10:20: 10:22 error: use of moved value: `*x`
boxed.rs:10     println!("{}", * x);
                               ^~
note: in expansion of format_args!
<std macros>:2:43: 2:76 note: expansion site
<std macros>:1:1: 2:78 note: in expansion of println!
boxed.rs:10:5: 10:24 note: expansion site
boxed.rs:9:13: 9:14 note: `x` moved here because it has type `Box<i32>`, which is non-copyable
boxed.rs:9     add_one(x);

Because a Box owns the resource, x will become invalid. If we try same in C++

#include <memory>
#include <cstdio>

void add_one(std::unique_ptr<int> num) {
    * num += 1;
}

int main() {
    auto x = std::make_unique<int>(5);
    printf("%i\n", *x);
    add_one(std::move(x));
    printf("%i\n", *x);
}

It will result in a segmentation fault:

% g++ -Wall -std=c++14 cpp_smart_pointers.cpp  -o cpp_smart_pointers
% ./cpp_smart_pointers
5
[1]    28585 segmentation fault (core dumped)  ./cpp_smart_pointers

Because the unique_pointer moved from x into the function argument x remains invalid and dereferencing it will leave it invalid. Note that g++ doesn't even issue a warning even when compiling this code with -Wall.

What I think is nice in the C++ version is the explicit std::move which makes it obvious to the reader, that x shouldn't be used afterwards.

The compile errors Rust gives can be intimidating when learning Rust, since it is not obvious from the code that x just moved.

But Rust's lifetime concept doesn't stop here. It even tracks down usage of invalidated references. Consider the following C++ code:

#include<cstdio>
#include<vector>

int main() {
    std::vector<int> v;
    v.push_back(5);
    int& x = v[0];
    printf("%i, ", x);
    v.push_back(6);
    printf("%i\n", x);
}

Executing this code will output 5, 0 on my machine. According to the C++ standard push_back invalidates any reference to elements (iterators). So accessing it triggers undefined behaviour.

Trying the same in Rust

fn main() {
    let mut v = vec![];
    v.push(5);
    let x = &v[0];
    print!("{}, ", x);
    v.push(6);
    println!("{}", x);
}

Triggers a compile error:

rust_dangling.rs:6:5: 6:6 error: cannot borrow `v` as mutable because it is also borrowed as immutable
rust_dangling.rs:6     v.push(6);
                       ^
rust_dangling.rs:4:14: 4:15 note: previous borrow of `v` occurs here; the immutable borrow prevents subsequent moves or mutable borrows of `v` until the borrow ends
rust_dangling.rs:4     let x = &v[0];
                                ^
rust_dangling.rs:8:2: 8:2 note: previous borrow ends here
rust_dangling.rs:1 fn main() {
...
rust_dangling.rs:8 }
                   ^

Rust tries to tell us, that we can't change the vector, because there is still a reference to it in scope, namely x! To fix the issue we need to limit the scope of x:

fn main() {
    let mut v = vec![];
    v.push(5);
    {
        let x = &v[0];
        print!("{}, ", x);
    }
    v.push(6);
    let x = &v[0];
    println!("{}", x);
}

Have comments? Discuss on Hacker News.

blogroll

social