RAII Footguns in Rust and C++
I’ve been getting back into C++ at Skydio, and I’ve twice lost several hours to debugging weird code behavior because of an RAII footgun in the language. A similar footgun is present in Rust, and I’ve been bitten by that too, so I figured I’d write down both.
RAII classes are often used to keep resources alive or hold locks for a given scope. There observable side-effects usually occur only in the constructor and destructor. However, it is quite easy in both languages to forget a tiny detail that will lead to the RAII object being destroyed too early.
C++: Forgetting to name an instance
What is wrong with this code?
#include <iostream>
#include <string>
class MyRAIIClass final {
public:
explicit MyRAIIClass(const std::string& name) : name_(name) {
std::cout << "Constructed " << name_ << "\n";
}
~MyRAIIClass() {
std::cout << "Destroyed " << name_ << "\n";
}
private:
std::string name_;
};
int main()
{
MyRAIIClass("OOPS");
std::cout << "Should be in RAII context\n";
}
I forgot to name the instance, which is completely valid. It creates a temporary object that is immediately destroyed, so that the RAII object has no effect for the rest of the scope. Since the RAII object has no other methods, it is very easy to make the mistake because you never refer to the object, and thus don’t look for a name. It is really easy to cause concurrency bugs this way, as you don’t actually hold a lock when you think you do! Louis Brandy has a dedicated section to this bug in his great CppCon talk.
The fix
There isn’t really a good time-of-use way to catch this in already existing
code.
clang-tidy
has a lint for some common cases, and is one way to catch this. Depending on
the compiler version you are using, C++17 introduces the [[nodiscard]]
annotation. If you are the author of the RAII class, you can annotate your
constructors with [[nodiscard]]
, which will cause the compiler to emit a
warning in such cases.
// Example program
#include <iostream>
#include <string>
class MyRAIIClass final {
public:
[[nodiscard]] explicit MyRAIIClass(const std::string& name) : name_(name) { std::cout << "Constructed " << name_ << "\n"; }
~MyRAIIClass() { std::cout << "Destroyed " << name_ << "\n"; }
private:
std::string name_;
};
int main()
{
MyRAIIClass ("OOPS");
std::cout << "Should be in RAII context\n";
}
Try running it. Notice the warning!
Rust: _
does not bind!
Rust has a similar footgun. It is compiler enforced convention in Rust to
prefix unused variables with _
. Since RAII variables are unused, but present
for side-effects, it is pretty common to use that style. Taking this to its
logical conclusion, you would think “oh, I don’t even need to give it a name,
let me just call it _
”. Do you think that works?
struct MyRAII {
name: String,
}
impl MyRAII {
pub fn new<S: Into<String>>(name: S) -> MyRAII {
let x = MyRAII { name: name.into() };
println!("Constructed {}", x.name);
x
}
}
impl Drop for MyRAII {
fn drop(&mut self) {
println!("Destroyed {}", self.name);
}
}
fn main() {
// let _r = MyRAII::new("Not OOPS");
let _ = MyRAII::new("OOPS");
println!("In RAII context");
}
Footgun! _
is different from _<anything>
! It does not
bind.
The fix
Unfortunately, there is no solution in Rust for this right now, apart from
training yourself to always name variables and avoid using _
. There was
discussion of introducing a
#[must_bind]
attribute, but that was rejected. I’m not aware of any clippy
lints either.