Thinking about how a less common C++ idiom should translate to Rust. Its common to write "cleanup" code in destructors in both C++ and Rust, but sometimes you have work that should only be done if it's time to cleanup "for the right reason."
For example, say you have a TempFile
struct, where TempFile::new()
creates a temporary file you can use for say a unit test, and it has a drop handler that deletes the file. However, when the test fails, you actually don't want to clean up the file -- you want to keep it so the developer can look at it and try to figure out why the test failed.
So if drop
is running because your #[test]
function returned Result::Err
, you want to keep the file. If it's running because your function returned Result::Ok
, you don't want to keep the file.
In C++, in a code base where you consistently use exceptions for error handling, you can use std::uncaught_exception
to detect if you are unwinding due to an exception being thrown:
struct TempFile {
~TempFile() {
if(!std::uncaught_exception()) {
this->delete_file();
}
}
};
This gives you the best of both worlds:
- By defining a destructor you can't forget to call delete_file when using TempFile
- The file is only deleted on success
AFAICT there is no way in a drop handler to distinguish if it's due to success or failure. I'm also not sure if it's possible to detect unwinding is happening due to panic. So I think I would have to force users to write:
let foo = TempFile::new();
test_work_that_may_fail(foo)?;
foo.success();
Where they have to remember to call success
or the cleanup won't happen. Is there a better way?
There are other examples of this pattern -- e.g. you want to create a file called "foo.pending" and then only once you've fully written it successfully rename it to "foo". This prevents other processes from accidentally trying to work on an incomplete file, either because the file is still being written or because the process writing it crashed and it shouldn't be used (you want the next stage of your pipeline to fail fast complaining the file isn't present, instead of silently appearing to work on empty/truncated data),
Footnote: I'm glossing over a detail here -- you would use std::uncaught_exceptions
(note the s) in real code in order to properly handle the case of a destructor running in response to an exception that itself has a try/catch inside of it. But then you have to save the starting value in the constructor and the snippet is longer/more-complicated. But the idea is the same.