RAII Footguns in Rust and C++

Posted on Oct 23, 2021

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";
}

Try running it

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");
}

Try it out.

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.