DEV Community

LowByteFox
LowByteFox

Posted on

Modern C++ or Rust - Basic borrowing system concept

Both C++ and Rust are fast, powerful programming languages. Both have pros and cons. This blog is my comparison and opinion of both programming languages by intermediate C, C++ and beginner Rust programmer.

Note:
I also code in TypeScript, JavaScript, Snail (Python) and couple other

Opinion

In my opinion with Modern C++ you may not need Rust. Why?
Short answer:
C++ has concept known as 'smart pointers', with them, you can easily imitate Rust's borrowing system mainly with unique_ptr

The long answer

It's not simple as unique_ptr, unique smart pointer has an issue and that is it can point to the same memory only once. That's why there is shared_ptr an another smart pointer that keeps track of how many of shared smart pointers are pointing to the same object in memory.

In rust

fn main() {
    let mut x = 4;
    let mut y = &mut x;
    let mut z = &mut x; // Cannot borrow more than once
}
Enter fullscreen mode Exit fullscreen mode

is the same as

#include <memory>

int main() {
    auto x = std::make_shared<int>(4);
    auto y = x;
    auto z = x;

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

In C++

However there is one issue. In rust, you can't borrow a variable as mutable more than one time. This is where C++ kind of wins, because you aren't limited to that restriction. However this gives you more boilerplate when getting the value out of the smart pointer.

Another example. What if we wanted to do something like

fn edit(val: &mut Vec<i32>) {
    val.push(7);
}

fn main() {
    let mut x = vec![4];
    // [4]
    edit(&mut x);
    // [4, 7]
}
Enter fullscreen mode Exit fullscreen mode

Well, very simply and this time let's use unique smart pointer

#include <vector>
#include <memory>

void edit(std::unique_ptr<std::vector<int>>& val) {
    val->push_back(7);
}

int main() {
    auto x = std::make_unique<std::vector<int>>();
    x->push_back(4);
    edit(x);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

This is one of couple possible solutions, you don't even need smart pointer for this, just make stack allocated object of vector and pass it as reference.

Now let's say you want to move it and make the function own it. In C++ use std::move function, simple!

Rust

fn eat(mut val: Vec<i32>) {
    val.push(7);
}

fn main() {
    let mut x = vec![4];
    eat(x);
    println!("{:?}", x); // X inaccesible
}
Enter fullscreen mode Exit fullscreen mode

C++

#include <cstdio>
#include <vector>
#include <memory>

void eat(std::unique_ptr<std::vector<int>> val) {
    val->push_back(7);
}

int main() {
    auto x = std::make_unique<std::vector<int>>();
    x->push_back(4);
    eat(std::move(x));
    printf("%ld\n", x->size()); // x becomes nullptr
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

This is where magic of Rust compiler comes to play, It won't compile saying that the value x was moved. C++ compiler will compile this however the application will crash because x will be turned into nullptr yikes. You can check it either comparing it with nullptr or switching to shared_ptr and making weak_ptr and calling expired on the weak smart pointer. Like so

    std::weak_ptr<std::vector<int>> hm = x;
    eat(std::move(x));
    printf("%d\n", hm.expired());
Enter fullscreen mode Exit fullscreen mode

But you shouldn't move shared_ptr 🙂
Don't mimic Rust's borrowing system, you'll be regretting it.

And we're at the end. I know i left out so many features of Rust but all can be done in C++ as well.

I showed you some examples on how to make your C++ safe as Rust can but with more boilerplate and complex overhead. I hope you liked the blog post! Let me know by leaving a comment 💖 As a bonus, i'll write some C++ memory safety tips

C++ memory safety tips

  1. new should not be used and is dangerous, because on some platforms new returns zero or throws an exception and that will turn into pile of problems. To avoid that, include new header like this #include <new> and change all new calls into new(std::nothrow) .... This way, if new fails, it will return nullptr.
  2. Switching from pointers to smart pointers takes a LOT of time and debugging and everything, so you can simply secure your current new calls, follow tip number 1
  3. When allocating array with new, to free every single value from the array, use delete[], not delete. With only delete you'll free only the first element of the array.

Top comments (3)

Collapse
 
aregtech profile image
Artak Avetyan

I prefer C++ and keep simple rules:

  1. Whenever possible declare objects to initialize in stack, instead of initializing via new operator;
  2. Whenever possible use references instead of pointers;
  3. Declare static singletons for global objects instead of initializing via new operator and declare them in a static methods instead of declaring in .cpp like a variable. Something like:

    class MyObject {
    public:
        static MyObject & getInstance();
    private:
        MyObject() = default;
        ~MyObject() = default;
    }
    ...........
    MyObject & MyObject::getInstance()
    {
        static MyObject _instance;
        return _instance;
    }
    
  4. Mainly use new operator, if:

    • A global object does not have default constructor;
    • Same instance of objects can be initialized multiple times and can be accessed at any time from any other object (for example, resources).
    • Need a chunk of buffer to store data and pass between threads
    • Couple of other cases :)

Such simple rules minimize memory risks. And use smart pointers only if indeed need.

Collapse
 
lowbytefox profile image
LowByteFox

I don't use smart pointers that much either. I do prefer the stack too - it's the fastest but sometimes, I do need to use pointers, for example when I deal with C functions or couple of other reasons. Being simple is always better and being safe is another plus.

Collapse
 
dyfet profile image
David Sugar

One area Rust does shine is in synchronized access to objects. In C++ one can have an object, a lock, and a guard, with no real association between these elements, so you can still access the object even without acquiring a lock. In Rust the mutex is part of the object, and the guard is also used as an accessor to the content, so the lifetime of the lock and object access are the same.

This is actually relatively easy to simulate in C++ for objects and containers with simple templating. The guard object then also becomes a kind of weak pointer and the object your locking becomes a private member along with the sync object so it can only be accessed thru an active guard pointer. As I have found it such a useful pattern I incorporated it as a simple header-only template in my latest moderncli library release.