DEV Community


Posted on • Updated on

Rust borrows for C/C++ Programmers

I did a lot of C and C++ early in my career. Among other thing, I worked on an RPG named "Mantra" with my high-school friends. I still have the source code, and looking back on it, I'm pretty impressed: good comments, nice structure, and even tests. I'm not sure the code I wrote today is 25 years better!

Pointers are used all over in C and C++, and a critical part of using them safely is to understand how long a pointer will be valid. Getting this wrong results in use-after-free bugs or memory leaks, both of which plague C/C++ programs.

C++ offers some tools to do this kind of thing automatically -- smart pointers, RAII, and so on. But in C, the language is generally no help, and it's up to the programmer to understand the situation. This is generally handled with long, explicit comments. From another major project I worked on, Amanda, this comment provides a good example:

* Note that any pointer-based attributes (strings, etc.)
* become owned by the XMsg object, and will be freed in
* xmsg_free.  The use of g_strdup() is advised for strings.
Enter fullscreen mode Exit fullscreen mode

Ownership and Borrowing

The comment above uses the term "owned". In this context, to "own" an object is to be responsible for freeing it when it is no longer useful. There can be only one owner, although ownership can be handed off.

It's common to use pointers (or, in C++, references) to point to a value "temporarily". Some of these patterns are so common that they don't warrant a comment. For example, consider

int calculate_score(board *Board, player *Player) { .. }
Enter fullscreen mode Exit fullscreen mode

In this case, even without any comments, it's clear that calculate_score can use board and player while it's calculating the score, but can't keep those pointers and use them later. If you're familiar with Rust, you'll know this as a "borrow".

Also implicit in this case is that board and player should only be accessed read-only in this context. Perhaps another thread is calculating a score for the same Board, so modifying the Board would lead to data races. Some codebases may use const for to enforce read-only access, but this is not universal.

Let's take another example:

// Find a player with the given name within the given board
Player *find_player(board *Board, name *char)
Enter fullscreen mode Exit fullscreen mode

Here there are two input pointers, and the result is also a pointer. A good C programmer looking at this declaration will infer that name is a read-only borrow, so the string is intact when find_player returns. They will probably also guess that the returned Player pointer is valid for the lifetime of the Board, and no longer.

There's lots of room for error, here! For example, if this is a multi-threaded application and players can join and leave the game, then the Player object's lifetime might be less than that of the Board. A misunderstanding like that might not cause issues until the timing works out just right (or just wrong!). C certainly won't do anything to prevent the issue.

Transitioning to Rust

Rust is designed to be safer than other systems languages like C and C++, but that comes at a perceived cost: fighting with the borrow checker. Most of the lessons about references and borrows in Rust present them as a safe kind of pointer.

But maybe the opposite perspective is useful -- maybe C/C++'s pointers are an unsafe way to implement borrowing. From this point of view, Rust is helping the programmer do something they've been trying to do for a long time with clearly-written comments and conventions.

This would have helped me when I was learning Rust. On learning about references, I jumped right to things that are best suited to smart pointers: Merkle trees and object caches. Experienced Rust devs will know that these tasks call for Rc or Arc, not plain references.

If I had realized that ownership and borrowing are just a formalization of concepts I already understood, I might have instead experimented with implementing designs in Rust that I had already implemented in C. I might have even implemented something like the Amanda Transfer Architecture as a learning exercise.

Hopefully, this perspective can be useful to other C/C++ programmers working to learn Rust.

Top comments (0)