Smart pointers: C++ and Rust
A heap value with one owner and a heap value with several owners are the two shapes a program needs, and both languages name them: unique_ptr and shared_ptr in C++, Box and Rc in Rust. The counts and the releases turn out to be the same; what differs is what the assignment does.
The task in C++
The program makes a shared string, copies the handle, prints the count, then resets one handle and prints the count again. It also makes a unique pointer to an integer and reads it, which is the single-owner shape.
Both smart pointers are classes with a destructor, so the release happens when the last handle goes out of scope, and the count is visible through use_count.
#include <iostream>
#include <memory>
#include <string>
int main()
{
auto shared = std::make_shared<std::string>("heap");
auto copy = shared;
std::cout << shared.use_count() << "\n";
std::cout << copy->size() << "\n";
copy.reset();
std::cout << shared.use_count() << "\n";
auto unique = std::make_unique<int>(42);
std::cout << *unique << "\n";
}
g++ -std=c++23 -Wall -Wextra shared.cpp -o shared
./shared
2
4
1
42
The count goes to two on the copy and back to one on the reset, and the string is four bytes long. The unique pointer prints forty-two through its dereference, and both values are released at the end of main without a delete.
The same task in Rust
The shared handle is Rc<String>, and Rc::clone is the call that increments the count. The single-owner handle is Box<i32>, which dereferences like a unique pointer and needs no call to free.
use std::rc::Rc;
fn main() {
let shared = Rc::new(String::from("heap"));
let copy = Rc::clone(&shared);
println!("{}", Rc::strong_count(&shared));
println!("{}", copy.len());
drop(copy);
println!("{}", Rc::strong_count(&shared));
let unique = Box::new(42);
println!("{}", unique);
}
rustc --edition 2024 shared.rs
./shared
2
4
1
42
The four lines are the same as the C++ output, which is the point of the comparison: the reference count, the length and the dereference all agree. drop takes the place of reset, and it is a move into a function rather than a method on the handle.
What differs and why
Who owns the value: unique_ptr and Box both mean one owner, and their assignment transfers ownership. shared_ptr and Rc both mean several owners, and their assignment is where the two languages part ways.
What an assignment does: a C++ copy constructor runs, the count increments and both handles stay usable. A Rust assignment moves the handle, the count does not change and the old binding is unusable. To increment the count in Rust the programmer writes Rc::clone, which is one word longer and makes the cost visible in the source.
What the count costs: an atomic increment in shared_ptr and in Arc, a plain increment in Rc. That is why Rust has two types where C++ has one, and why the single-threaded code pays nothing for the thread safety it does not use.
What the compiler knows: a shared_ptr may be empty and dereferencing it is the programmer's risk, while Rc<T> always holds a value and Option<Rc<T>> is the empty form. The same distinction as T* against Option<&T> appears one level up.
The habit that breaks
The habit is the line that copies a shared pointer: auto copy = shared;. It compiles, increments the count and leaves both names valid, and it is what every C++ programmer writes without thinking.
#include <iostream>
#include <memory>
int main()
{
auto shared = std::make_shared<int>(7);
auto copy = shared;
std::cout << *shared + *copy << "\n";
}
g++ -std=c++23 -Wall -Wextra move.cpp -o move
./move
14
The same line in Rust moves the handle, and the next use of the first name is an error. The compiler not only reports it, it also names the call that was meant.
use std::rc::Rc;
fn main() {
let shared = Rc::new(7);
let copy = shared;
println!("{}", *shared + *copy);
}
rustc --edition 2024 move.rs
error[E0382]: the type `Rc` does not implement `Copy`
--> move.rs:6:21
|
4 | let shared = Rc::new(7);
| ------ this move could be avoided by cloning the original `Rc`, which is inexpensive
5 | let copy = shared;
| ------ value moved here
6 | println!("{}", *shared + *copy);
| ^^^^^^ value borrowed here after move
|
= note: consider using `Rc::clone`
= note: borrow occurs due to deref coercion to `i32`
help: clone the value to increment its reference count
|
5 | let copy = shared.clone();
| ++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0382`.
The note about the cheap clone and the suggestion on line five are the whole fix. A C++ compiler cannot offer it, because the copy it performs is exactly what the language says an assignment does; the Rust compiler stops at the line where the two habits disagree.
The Rust lesson for this ground is Box: values on the heap, and the lesson on Rc and RefCell continues with the shared mutation. The C++ lesson is std::unique_ptr, and std::shared_ptr and std::weak_ptr covers the counted pointer. The previous page in this set is Memory layout.
The C++ programs were built with g++ 15.2.0 and the Rust programs with rustc 1.96.1, and both were tested on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev