I guess I'm not the first one who was thinking about following problem, but I didn't find a good solution, so here it comes.
Let's consider a simple code snippet:
let mut file = std::fs::File::create("test")?;
file.write(&b"hello"[..])?;
What happens when file
is dropped? Well, the documentation tells that all errors, which may happen during the operation, are silently ignored and sync_all
should be used before dropping it if the errors must be handled.
Let's go a level up. What if I have a generic method that uses std::io::Write
instead? Judging from the approach that BufWriter
takes, it is a good idea to call flush
explictly before letting it drop, but it is just a desperate attempt to save the day, there is nothing to rely on.
We're closing to the point which led me to this post. I implemented a codec for bitwise compression (a task from Rosetta Code actually). The implementation provide a stream-like interface, which is convenient for some use cases, but I wanted to provide yet adapters for Read
and Write
, which would work better for files etc. With Write
I got stuck. The last byte of the bit stream may need padding, but the padding must be emitted only when the stream ends. Where it should be emitted?
- It can't be emitted in
flush
, because it would corrupt the data unlessflush
was called only just before dropping the the output. So,flush
for such an adapter never flushes really everything. - The right place to emit the padding would be the close operation. But which? If I make an implementation-specific one, it would be useless for generic implementations working with just
Write
(which is one of the points mentioned above). - If emitting the padding is postponed to
drop
and the operation fails, I mean the underlyingWrite::write
fails, the client code never learns that the written data are incomplete (and thus corrupted). And implementing it indrop
seems tricky to me anyway – I'd rather avoid it.
In general, I consider RAII really fine and matching most use cases well, but I have strong doubts whether it is good enough for the cases when the destructor should execute an operation that might fail and the error should be known to the caller.
What is the best practice and recommendation here? From the little what I saw, I'm not sure if there is really an established general way to cope with that.
Just to write down my current thoughts, I share a bold suggestion. (Well, my background is Java – you were warned –, therefore the similarity with Java's Closeable
is not a coincidence.)
Would not be worth having a trait like this TryDrop
(I considered naming it Close
, which suits better the described case, but I think it might have more general uses):
trait TryDrop {
type Result;
fn try_drop(self) -> Self::Result;
}
Such a trait would provide a common way to dispose resources, which might fail, and still indicate the problem. I guess it could be possible to provide some sensible default implementations, e.g., for Write
(to flush
before drop
) and other types like BufReader
or File
might provide even better variant that leverages their specifics. For instance:
impl TryDrop for File {
type Result = std::io::Result<()>;
fn try_drop(self) -> Self::Result {
self.sync_all()
}
}
Further I guess that it would be nice to have Clippy warning when a type implementing this trait is simply dropped. The warning should point out that any failures of the destructor would be ignored and it is recommended to use the method of this trait to dispose the resource and handle the error safely.