How 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.
> 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.
I 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.
Good 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<>>`".