Remix.run Logo
winternewt 2 hours ago

Destructors are vastly superior to the finally keyword because they only require us to remember a single time to release resources (in the destructor) as opposed to every finally clause. For example, a file always closes itself when it goes out of scope instead of having to be explicitly closed by the person who opened the file. Syntax is also less cluttered with less indentation, especially when multiple objects are created that require nested try... finally blocks. Not to mention how branching and conditional initialization complicates things. You can often pair up constructors with destructors in the code so that it becomes very obvious when resource acquisition and release do not match up.

sigwinch28 2 hours ago | parent | next [-]

A writable file closing itself when it goes out of scope is usually not great, since errors can occur when closing the file, especially when using networked file systems.

https://github.com/isocpp/CppCoreGuidelines/issues/2203

mort96 10 minutes ago | parent | next [-]

You need to close it and check for errors as part of the happy path. But it's great that in the error path (be that using an early return or throwing an exception), you can just forget about the file and you will never leak a file descriptor.

You may need to unlink the file in the error path, but that's best handled in the destructor of a class which encapsulates the whole "write to a temp file, rename into place, unlink on error" flow.

leni536 21 minutes ago | parent | prev [-]

Any fallible cleanup function is awkward, regardless of error handling mechanism.

yoshuaw 2 hours ago | parent | prev | next [-]

I couldn't agree more. And in the rare cases where destructors do need to be created inline, it's not hard to combine destructors with closures into library types.

To point at one example: we recently added `std::mem::DropGuard` [1] to Rust nightly. This makes it easy to quickly create (and dismiss) destructors inline, without the need for any extra keywords or language support.

[1]: https://doc.rust-lang.org/nightly/std/mem/struct.DropGuard.h...

mandarax8 an hour ago | parent | prev | next [-]

The entire point of the article is that you cannot throw from a destructor. Now how do you signal that closing/writing the file in the destructor failed?

DonHopkins 19 minutes ago | parent [-]

That tastes like leftover casserole instead of pizza.

jchw 2 hours ago | parent | prev | next [-]

Destructors and finally clauses serve different purposes IMO. Most of the languages that have finally clauses also have destructors.

> Syntax is also less cluttered with less indentation, especially when multiple objects are created that require nested try... finally blocks.

I think that's more of a point against try...catch/maybe exceptions as a whole, rather than the finally block. (Though I do agree with that. I dislike that aspect of exceptions, and generally prefer something closer to std::expected or Rust Result.)

mort96 2 hours ago | parent [-]

> Most of the languages that have finally clauses also have destructors.

Hm, is that true? I know of finally from Java, JavaScript, C# and Python, and none of them have proper destructors. I mean some of them have object finalizers which can be used to clean up resources whenever the garbage collector comes around to collect the object, but those are not remotely similar to destructors which typically run deterministically at the end of a scope. Python's 'with' syntax comes to mind, but that's very different from C++ and Rust style destructors since you have to explicitly ask the language to clean up resources with special syntax.

Which languages am I missing which have both try..finally and destructors?

brewmarche an hour ago | parent | next [-]

In C# the closest analogue to a C++ destructor would probably be a `using` block. You’d have to remember to write `using` in front of it, but there are static analysers for this. It gets translated to a `try`–`finally` block under the hood, which calls `Dispose` in `finally`.

    using (var foo = new Foo())
    {
    }
    // foo.Dispose() gets called here, even if there is an exception
Or, to avoid nesting:

    using var foo = new Foo(); // same but scoped to closest current scope
These also is `await using` in case the cleanup is async (`await foo.DisposeAsync()`)

I think Java has something similar called try with resources.

jchw 2 hours ago | parent | prev | next [-]

I don't view finalizers and destructors as different concepts. The notion only matters if you actually need cleanup behavior to be deterministic rather than just eventual, or you are dealing with something like thread locals. (Historically, C# even simply called them destructors.)

mort96 an hour ago | parent | next [-]

There's a huge difference in programming model. You can rely on C++ or Rust destructors to free GPU memory, close sockets, free memory owned through an opaque pointer obtained through FFI, implement reference counting, etc.

I've had the displeasure of fixing a Go code base where finalizers were actively used to free opaque C memory and GPU memory. The Go garbage collector obviously didn't consider it high priority to free these 8-byte objects which just wrap a pointer, because it didn't know that the objects were keeping tens of megabytes of C or GPU memory alive. I had to touch so much code to explicitly call Destroy methods in defer blocks to avoid running out of memory.

adrianN an hour ago | parent | prev | next [-]

Sometimes „eventually“ is „At the end of the process“. For many resources this is not acceptable.

rramadass an hour ago | parent | prev [-]

> I don't view finalizers and destructors as different concepts.

They are fundamentally different concepts.

See Destructors, Finalizers, and Synchronization by Hans Boehm - https://dl.acm.org/doi/10.1145/604131.604153

dist-epoch 2 hours ago | parent | prev [-]

Technically CPython has deterministic destructors, __del__ always gets called immediately when ref count goes to zero, but it's just an implementation detail, not a language spec thing.

raverbashing an hour ago | parent | prev | next [-]

But they're addressing different problems

Sure destructors are great but you still want a "finally" for stuff you can't do in a destructor

dist-epoch 2 hours ago | parent | prev [-]

Python has that too, it's called a context manager, basically the same thing as C++ RAII.

You can argue that RAII is more elegant, because it doesn't add one mandatory indentation level.

logicchains an hour ago | parent [-]

It's not the same thing at all because you have to remember to use the context manager, while in C++ the user doesn't need to write any extra code to use the destructor, it just happens automatically.