3 ms·
> GC only solves the application memory part, thus doesn't help at all for handling those external resources. This is far from entirely true. Most languages wi
by samatman 3y ago
> GC only solves the application memory part, thus doesn't help at all for handling those external resources.
This is far from entirely true. Most languages with a garbage collector have finalizers, which will clean that sort of thing up. Generally, and unlike memory allocation, one can call those finalizers from within user code, so as not to have to rely on the GC running.
The distinction between reference counting and garbage collection is an artificial one. Reference counting is an approach to garbage collection, one with different tradeoffs from more concept-central algorithms for GC. I agree with you that it's a more unified approach to resource management, finalizers require user attention in a way which ordinary allocation doesn't, so yes, system resources get treated differently. I don't see that as a slam-dunk against them, however.
- o11c 3y agoOne problem with finalizers, try-finally, try-with-resources, Python-`with`, etc. is that they don't actually guarantee the code will be called even in common cases. In particular, any function that returns a (possibly newly-constructed) open file handle has not yet informed the caller that it's actually a thing to keep track of, unlike with destructors which are active immediately once the object's lifetime begins, and only rely on `move` being relatively atomic.
- kaba0 3y agoHow is a try-with-resources not guaranteed? At least in Java, it is guaranteed to run - plus, you can get your closing/destructor logic wrong in RAII languages as well.
- cesarb 3y ago> How is a try-with-resources not guaranteed? At least in Java, it is guaranteed to run There's a subtle detail you have to be careful with, however: if you try to allocate memory or call a method between allocating your resource and actually doing the try-with-resources, you might get an OutOfMemoryError or a StackOverflowError, and your resource will leak. That is, if you do something like: try (MyHolder holder = new MyHolder(allocateResource())) { ... } You can have a leak if the memory allocation in the "new MyHolder()" fails, or if there's not enough space in the stack to call the MyHolder constructor. > plus, you can get your closing/destructor logic wrong in RAII languages as well. For instance, in C++ you can accidentally put your resource in a temporary which is immediately destructed at the end of the line, when you wanted it to last until the end of the enclosing scope. Rust makes it harder for this to happen, but it's still possible.
- o11c 3y agoI mean, it's possible to write bad code in C++. But at least it's possible to write good code too. C++ makes a careful sequence that's hard to get wrong and easily compiler enforced: * if a given subobject (field or base class)'s constructor runs, its destructor is guaranteed to run. * in particular, in low-level ownership classes, you can arrange for field initializers to be `noexcept`, so you get to run their dtor, regardless of subsequent manipulation of the fields - just be sure to not assume invariants from the fully-constructed main object case. In most classes, deferring all ownership logic to the fields is simpler - rule of zero beats rule of 5. And if you do add manual try-catch during construction it will actually work. Languages other than C++ generally fail in several ways: * Allow exceptions to be thrown by the runtime for reasons unrelated to the code being executed * No support for subobjects, forcing all objects to be allocated separately and thus the runtime not knowing it needs to poke them since finalizers are only applied to objects not pointers. In particular, Python's `contextlib.ExitStack` can narrow the window in which case leaks are possible, but not eliminate it. Rust does slightly better than C++ by enforcing trivial moves, at the cost of making many useful programs impossible to write.
- worik 3y agoOuch! > Rust does slightly better than C++ by enforcing trivial moves, at the cost of making many useful programs impossible to write. What "useful" programs fit a useful definition of "many"? It makes self referential looped data structures hard to write (double linked lists, trees with pointers to parents). No loss. Great improvement to have fewer of those It makes arbitrary allocation hard. Enforces behaviors. Makes a programmer think very hard. "useful programs impossible to write. " No it does not. It ,ages a lot of bad ideas hard to implement, makes nothing "impossible "
- o11c 3y agoGood luck implementing "peer pointers" in Rust. Zero allocation involved, trivial to do safely in C++. class A { /* can be NULL if detached */ B *peer; /* other fields generally exist in one of the classes. */ /* useful objects are typically created in pairs, but can be constructed detached if needed */ /* move ctor and swap keep the (value) objects pointing at each other */ /* move assignment and dtor call detach() on the overwritten/expiring object */ /* Often separate "detach and cancel" and "detach without canceling" are useful */ void detach(); /* other functions specific to the peerage */ }; class B { A *peer; /* same API as `class A`, but usually most of it is only used via one owner. */ /* In my experience, generally one class's instance goes in some collection of centralized objects like an event loop, and the other is used as a handle to it. */ }; I guess we could always use the usual Rust solution of "screw ownership and efficiency, just shove everything in an `Rc<RefCell<>>`".
- pkolaczk 3y agoTry-with-resources doesn’t guard against use-after-close. Rust RAII does.
- pjmlp 3y agoYou use the C and C++ approach to all their design flaws and reach out to a static analysis tool that covers such cases.
- cesarb 3y ago> Most languages with a garbage collector have finalizers, which will clean that sort of thing up. Using finalizers for cleanup of non-memory resources is bad because they will only be called when there's memory pressure. If you have used all of your non-memory resource, but there's still plenty of free memory, the allocation of that resource will fail; if you instead force a garbage collection at that point, not only will you cause a pause, but also you will be collecting memory while there's still plenty of it available.
- zozbot234 3y agoFinalizers will be non-deterministic when called as part of GC. One advantage of reference counting is that it is deterministic (and yes, this means that sometimes freeing the last reference that's keeping a bunch of objects around will lead to a spike of extra deallocations. Guess what, that's what determinism is. It's implied by the need to free resources promptly.)
- PhilipRoman 3y agoTrusting the GC with freeing externally observable resources will bring you some very painful bugs. On that note, it's 2024 and we still cannot unmap a mmapped file from Java in a timely manner. I really hope they do something about it.