A Practical Approach
I’ve been writing C++ on and off professionally for maybe 10 years now, and started to use Rust about 2 to 3 years ago.
Here I’d like to share my experience and thoughts on the transition between the two languages with code examples to illustrate the differences.
Disclaimer: This article is not a C++ vs Rust comparison. It’s just my personal experience and opinions about things that are important to me, not the engineering community in general.
The type of work you do influences your perception
I think that the type of work you do highly influences your perception about a language and technology.
With C++, I have spent a lot of my time writing tools for different systems and in the most recent years I have dove into game development with C++ also. Both of these applications can be very different in the way they assume ownership, manage memory, use system calls and interact with the kernel. The user experience is also very different depending on the case, where some case performance matters a lot more than others.
As for Rust I started my experience working on Open Source projects and took upon myself to develop an entire database from scratch which is the perfect application for the language, since memory management and ownership are critical for any database engine.
You can find the project here: https://github.com/joaoh82/rust_sqlite
Now to the important points:
Memory Safety: Ownership and Borrowing
One of the most well-known features of Rust is its memory safety guarantees. To illustrate this difference, let’s consider a simple example in C++ and Rust.
// C++
#include <iostream>
#include <vector>
void foo(std::vector<int>& vec) {
vec.push_back(42);
}
int main() {
std::vector<int> vec = {1, 2, 3};
foo(vec);
std::cout << vec.back() << std::endl;
return 0;
}#include #include
// Rust
fn foo(vec: &mut Vec<i32>) {
vec.push(42);
}
fn main() {
let mut vec = vec![1, 2, 3];
foo(&mut vec);
println!("{}", vec.last().unwrap());
}
The Rust code is more verbose, but it’s also more explicit about the ownership and borrowing of resources. The &mut
keyword in Rust ensures that the vec
reference is mutable, and there's no risk of accidentally modifying the original data.
The Compiler:
Error messages from both C++ and Rust compilers can be daunting and necessitate effort to comprehend and address appropriately. However, the underlying reasons for this vary between the two languages.
In C++, error messages can be so lengthy that their size is measured in kilobytes. An infinite scroll feature in your terminal emulator becomes essential, as the compiler generates copious amounts of text. Over time, you develop an intuition for whether it’s more productive to read the error message or scrutinize your code based on the message’s size. Generally, the larger the error, the more effective it is to simply examine your code. Resolving this issue may necessitate altering the way C++ templates are defined.
Conversely, in Rust, encountering a compiler error (after addressing any glaring typos) often spells trouble. These errors typically suggest that you need to modify your code structure or invest time in refining lifetimes to ensure memory safety. While this process can be time-consuming and frustrating, it’s crucial to heed the compiler’s guidance. Embracing this humbling experience often results in superior code. Additionally, Rust’s error messages are concise enough to fit on a single screen, which is a pleasant bonus.
Build System: Cargo
Rust’s build system, Cargo, is a breath of fresh air compared to C++’s disparate build systems. To demonstrate this, let’s look at a simple example of adding a dependency.
In Rust, you just need to add a line in the Cargo.toml
file:
[dependencies]
serde = "1.0.130"
In C++, you’d need to modify your build files, which could be CMake, Bazel, or something else, often requiring more steps and knowledge of each build system.
Type System: Generics and Enums
Rust’s type system allows for more expressive and safe code. Let’s look at an example of generics with traits in Rust compared to templates in C++.
// C++
#include <iostream>
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << std::endl;
return 0;
}
// Rust
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
a + b
}
fn main() {
println!("{}", add(1, 2));
}
In Rust, traits clearly indicate the contract expected from the type, making the code easier to understand and reason about.
Enums in Rust are also more powerful than their C++ counterparts. Here’s an example using the Result
enum:
// C++
#include <iostream>
#include <stdexcept>
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}
int main() {
try {
std::cout << divide(10, 0) << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
// Rust
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("Division by zero")
} else {
Ok(a / b)
}
}
fn main() {
match divide(10, 0) {
Ok(result) => println!("{}", result),
Err(e) => eprintln!("Error: {}", e),
}
}
In Rust, the Result
enum is used as a common way to express fallible computations, making error handling more consistent and predictable across different libraries.
Conclusion
In conclusion, transitioning from C++ to Rust has been a positive experience for me. To be honest, I haven’t really transitioned I just now have Rust also under my belt as I have C++, since I still use both extensively. That said, rust’s memory safety guarantees, powerful type system, and consistent build system have made my day-to-day developer experience more enjoyable. While Rust’s syntax may be more verbose, the benefits it provides in terms of safety and expressiveness are well worth it.
Happy developing!
Be sure to leave a CLAP and of course comments are always welcome!
Cheers!