Yes! That's basically it, perhaps with some small differences, but the idea is the same. It seems I'm not the first one indeed Thank you for the link.
I'll try to comment the other replies as well and it seems to me that they mostly miss the point. So I'd like to emphasize a few concerns:
- The first big question: how to deal with destructors that should perform an operation that may fail and how to not let the failure pass silently?
- The second big question: how to make such a kind of destructors composable?
The particular example with the bitwise I/O just shows a prominent example where std::io::Write
alone lacks something that would make implementation of better, more powerful and more generic decorators possible. And I wondered if I missed something which would make my suggestion irrelevant.
It is a kind of workaround. It is applicable in some scenarios, but actually answers none of the big questions. How much impractical it could be? Just imagine BufWriter
implemented in that way.
I think I don't get it. And it sounds like something actually more and unnecessarily complex than the suggestion with TryDrop
. I could be wrong though – an example, please?
I see the only significant difference from my TryDrop
suggestion that yours takes &mut self
. But this is a very important difference, because it does not prevent using the "tried to drop" instance, which means that it has to mark and check its invalid inner state. Which I would like to prevent – and that's why I suggest taking self
instead. (Btw. the error type could possibly even return self
alongside with the error detail in the case of recoverable errors… and if the client does not care, it can be simply dropped hard then.)
Yes, I considered it. It is basically similar to @pcpthm's suggestion, right? So I don't see it as a real solution. Both have one common property: they put the burden on the client of the code and force the client to take care of details that the composed type should be able to handle on its own.
To make it more explicit – assuming Write: TryDrop<Result = std::io::Result>
, we could write:
fn write_file<W: Write>(mut f: W) -> std::io::Result<()> {
f.write_all(&b"Hello"[..])?;
f.try_drop()?
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Err(e) = write_file(MyCompressingWriter::from(File::create("test.bin")?)) {
println!("File corrupted, because an error occurred: {}", e);
}
Ok(())
}
In this simple case, a first attempt to use some into_inner
which performs the last write of the decorator and returns the underlying Write
implementation:
fn write_file<W: Write>(f: &mut W) -> std::io::Result<()> {
f.write_all(&b"Hello"[..])?;
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut f = MyCompressingWriter::from(File::create("test.bin")?);
write_file(&mut f)?; // Ehm, we should handle it too, right?
let f = f.into_inner()?; // And again…
f.sync_all()?; // And again… I suppose another function should wrap it all
Ok(())
}
And yet the code must know that there is something like into_inner
specific for MyCompressingWriter
.
Obviously, write_file
stands here for anything arbitrarily complex which takes the ownership of the resource to write some data in. I guess that in such a case the workaround will not work anymore.