Avoiding single-threaded memory access bugs with Rust (for C++ developers)

In a previous article, I showed how Rust prevents us from introducing race conditions and invalid memory access to our code in multi-threaded contexts. In this article, we will look at several kinds of memory access bugs in single-threaded C++ and how Rust prevents us from making these mistakes.

Returning references to temporaries

Returning references to temporaries causes the callers of our functions to access invalid memory, and either crash their application or worse, overwrite random memory and cause a hard to debug error later. References can also be hidden inside classes we may return, whether they’re our own or from the standard library.

A great new standard library type from C++17 std::string_view lets us pass references to strings or parts of strings cheaply and efficiently. Unless you want to gain ownership of a std::string, you very likely want to pass this type instead of const std::string& to your functions. The downside of std::string_view is that we have to make sure the memory we point to stays alive at least as long as the view does. If we return a string view from a temporary string, we get undefined behavior when we try to read from it.

Rust’s type &str is the equivalent of std::string_view: it is an unowned slice of a string. Rust’s borrow checker will ensure that the underlying data lives long enough.

Short lifetimes

In C++, we have to make sure that the long-living references we store inside structs and classes are valid as long as we’re using them. In trivial cases, this can be easily achieved. In nontrivial cases, mistakes can easily happen and trigger UB.

In C++ code, these bugs are often very similar (or even the same) as returning references to temporaries: we’d have to resort to delayed initialization of our TempAgency (with optional or unique_ptr) if we didn’t want to return references to temporaries as described in the previous section.

Rust’s borrow checker exists to prevent exactly these cases. The things we reference must outlive structs that reference them.

References to container contents

It is sometimes convenient to store a reference to something in our function. It may be expensive to compute the address of that reference, or it may just help us to stop repeating ourselves. When we store a reference to contents of containers, however, there are always operations on those containers that invalidate these references. We have to rely on manually checking that all of our references stay valid until we have stopped using them.

In Rust, the borrow checker prevents us from borrowing the vector mutably while we have it borrowed immutably. Not only are we guaranteed that the first_thing doesn’t change, we are also guaranteed that the vector itself won’t be changed!

Tricky lifetime extensions

In C++, when storing the result of an expression in a const T& or a T&&, the temporary’s object’s lifetime is automatically extended. This allows us to iterate over temporaries like this in for-range loops (the iterated-over expression is stored in an auto&& variable):

This lifetime extension unfortunately only applies to the expression itself, not to any intermediary values. If our expression gets a reference from a temporary object, our reference will point to invalid data! This means that if we iterated over characters of the last string from get_huge_data, that string would have already been freed.

Thanks to the borrow checker, we don’t even need to look up lifetime extension rules in Rust, and we can simply try compiling this: if the lifetime gets extended, we are happy, and if it doesn’t we would get an error about using a freed object.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store