RAII and Drop: C++ and Rust
RAII is the same idea in both languages: tie a resource to an object and release it in the destructor. The difference is what happens when the object is passed to a function. C++ copies it unless the class says otherwise, so the destructor runs twice; Rust moves it, so the destructor runs once, in the function that owns it.
The program below opens a guard, returns early from a function and passes the guard to another function. Both versions are built and run, and the last lines of the output are the whole lesson.
The task in C++
The guard prints when it is constructed and when it is destroyed. work returns early in one call and finishes in the other, and the destructor runs on both paths. That part is RAII working exactly as intended, and it is why C++ needed no garbage collector for this job.
Then main passes the guard to use by value. The class has no move constructor, so the compiler uses the copy constructor, and two objects now believe they own the resource. The output ends with two closes and one of them is wrong.
#include <iostream>
struct Guard
{
const char* name;
explicit Guard(const char* n) : name(n) { std::cout << "open " << name << '\n'; }
~Guard() { std::cout << "close " << name << '\n'; }
};
void use(Guard copy)
{
std::cout << "using " << copy.name << '\n';
}
void work(bool stop)
{
Guard guard{"file"};
if (stop) {
std::cout << "early return\n";
return;
}
std::cout << "work done\n";
}
int main()
{
work(true);
work(false);
Guard shared{"db"};
use(shared);
std::cout << "after use\n";
}
g++ -std=c++23 -Wall -Wextra raii.cpp -o raii
./raii
open file
early return
close file
open file
work done
close file
open db
using db
close db
after use
close db
No warning and no error from the compiler: the copy is legal, and the class is simply not written to prevent it. A real file handle would be closed twice, and the second close is undefined behaviour.
The same task in Rust
The Rust guard implements Drop instead of a destructor and is passed by value. A type with a destructor cannot be Copy, so passing it moves it: the callee owns it and drops it once, and the caller has nothing left to drop. The same program prints one close per guard.
The early return still drops, because the compiler places the drop at the end of the scope on every path out. The output is the same as the C++ one until the last two lines.
struct Guard {
name: &'static str,
}
impl Guard {
fn new(name: &'static str) -> Self {
println!("open {name}");
Guard { name }
}
}
impl Drop for Guard {
fn drop(&mut self) {
println!("close {}", self.name);
}
}
fn use_guard(guard: Guard) {
println!("using {}", guard.name);
}
fn work(stop: bool) {
let _guard = Guard::new("file");
if stop {
println!("early return");
return;
}
println!("work done");
}
fn main() {
work(true);
work(false);
let shared = Guard::new("db");
use_guard(shared);
println!("after use");
}
rustc --edition 2024 raii.rs
./raii
open file
early return
close file
open file
work done
close file
open db
using db
close db
after use
There is no move constructor to write and no rule of five to remember. The default behaviour is the correct one, and a type that must not be moved has to opt out explicitly, which is rare.
What differs and why
Who owns the guard: in C++ a by-value parameter is a new object, and the class decides whether that is a copy or a move. In Rust a by-value parameter is the same object in a new binding, because assignment and argument passing are moves for any type that is not Copy.
What the compiler rejects: C++ accepts the copy and warns about nothing unless the class deletes the copy constructor. Rust rejects the copy because a type with a destructor cannot be Copy, and a second close becomes impossible rather than undefined.
What happens at run time: the C++ program closes twice, which is the bug; the Rust program closes once, in the function that last owned the guard. The destructor order for locals is the reverse of declaration in both languages, so the rest of the output matches.
The habit that breaks
The habit is to write a resource class without thinking about copying, because the compiler does not force the question. The Rust compiler forces it: adding a Drop implementation removes the implicit Copy, and any attempt to copy stops with E0204. The fix in C++ is the rule of five or a deleted copy constructor; the fix in Rust is nothing, because the default is already move-only.
The C++ lesson for this ground is RAII and the rule of zero and five; the Rust lesson is Drop, scope and RAII.
The C++ program was built with g++ 15.2.0 and the Rust program with rustc 1.96.1, and both were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev