> destructor leaks
Rust doesn't leak destructors any more than C++ (including the version of C++ presented in the OP). The only way to leak a destructor accidentally is to create a cycle (which is quite difficult to do, essentially requiring you to stuff a RefCell inside an Rc) or to have some data owned by a thread that's eternally stalled. > reliance on macros
Individual macros are generally regarded as convenient workarounds rather than ideal long-term solutions, and anywhere that you see pervasive macro use tends to be ripe for an RFC for a dedicated feature to subsume it. > the tremendous complexity of traits
No denying that traits are Rust's "deepest" feature, but that's because of a general philosophy to leverage traits to provide newly desired bits of functionality rather than introducing all-new language concepts. Removing functionality from traits wouldn't result in making the language less complex; that functionality would just end up going elsewhere.Yes, the Waterbed Theory of Complexity[1]. It can be useful to funnel complexity into specific areas where it's at least expected, and there may exist better mechanisms for dealing with it.
Right, I'm not saying C++ is perfect either. I feel like Rust made a wrong turn here: https://www.reddit.com/r/rust/comments/3404ml/prepooping_you...
> reliance on macros
I'm referring specifically to try!.
None of the proposed solutions mitigated this either. You will always be able to leak cycles. The difference was that some proposals made it possible to mark types as non-leakable, in which case they can't be placed in things which may potentially leak.
The fact that things will leak was never under question there. The decision to be made was on how safety should interact with it -- should there be a way for a type to rely on its destructor being run for the purposes of safety? The decision was that there shouldn't, and we ended up fixing two APIs which relied on destructors being run for safety, instead of adding a major language change.
Maybe the cleanest solution to the leakpocalypse would be to mark RefCell.borrow_mut as unsafe, because it's problematic in two separate ways (it can both panic and create cycles). That way the safe fragment of Rust would guarantee that memory leaks can't happen and destructors always run. That would fit with the spirit of Rust, which strongly prefers 100% guarantees to 99% guarantees. Though I guess that idea was judged too inconvenient for users, and I don't claim to know what users want.
One way to handle this is to have each type explicitly opt-in to saying they're concurrent-safe, but this is fairly annoying, as it requires a lot of boilerplate. Boilerplate that is mostly pointless in Rust, because its rules mean most types people define will be safe, and most types that aren't safe will be built out of existing types that aren't safe. OIBITs are proxies for "safe", and are designed to strict a balance, by basically following that rule (types made out of types implementing an OIBIT automatically impl that OIBIT too).
Moving on to your specific suggestion: RefCell isn't the only way to not run destructors, for instance, one can create ownership cycles with Mutex and RWLock in a similar way, and, more broadly, threads that dead lock (or live lock) will leak their resources, as would calling std::process::exit. There's also weirder/wilder things like the following: suppose Queue<T> is a multiple-producer multiple-consumer queue type, where any instance can act as both a producer and a consumer (typical for such types), then the following code creates an ownership cycle within the queue, and so won't run the destructor of X:
struct Msg {
x: X,
y: Queue<Msg>
}
let q: Queue<Msg> = ...;
let q2 = q.clone(); // another handle to the same queue
q.send(Msg { x: ..., y: q2 });
drop(q);
Now, sure you could mark all these things unsafe, but I think that starts to make things very inconvenient, and, somewhat, weaken what `unsafe` means (it normalise its use since those things are typically totally fine). (Also, those examples are almost surely not exhaustive.)1) After some soul searching, it seems like the really important invariant to me is that all alive objects should be reachable from the stack(s) at any given moment. So I'm kind of okay with failing to run destructors due to deadlock or process exit, but not okay with leaking a cycle. Is that a sensible distinction, or is the situation even more subtle?
2) How do I make a cycle with Mutex and RWLock?
3) I don't completely understand the queue example, what exactly is the magic that allows self-reference?
I think that's a reasonable distinction, but, at least for deadlocks, I'm not sure it is enough to have all the guarantees one might want (particularly around multithreading/scoping). Then again, it might be.
> 2) How do I make a cycle with Mutex and RWLock?
The same as a RefCell, just call `.lock().unwrap()` and `.write().unwrap()` instead of `.borrow_mut()`. These two types are essentially the same as a RefCell, just with more synchronisation, and fewer panics. They can block and wait for another thread to finish, at the risk of deadlocks of course, while a RefCell is restricted to a single thread, so blocking would just dead lock a thread with itself, hence a panic resolves the situation better. In fact, other than this blocking/panicking distinction, a RwLock and RefCell are almost identical: .read == .borrow and .write == .borrow_mut.
Here's a demo which creates cycles that contain a type that prints in the destructor (demonstrating that they don't run): https://play.rust-lang.org/?gist=6ea16b859e22f57fee14&versio...
> 3) I don't completely understand the queue example, what exactly is the magic that allows self-reference?
This is also essentially the same as the core RefCell/Mutex example, replacing .borrow_mut()/.lock() with .send. An mpmc Queue<T> will be isomorphic to Arc<Mutex<VecDeque<T>>> (although unlikely to be implemented as such in practice). The key is that they will reference-count the book-keeping that needs to be shared among all handles (like the start/tail of the queue), which allows creating a reference cycle.
> I feel like Rust made a wrong turn here
I think the broader context of "the leakpocalypse" involved a host of difficult decisions, all of them suboptimal in their own ways. Of all the options on the table, it's my opinion that Rust took the most pragmatic one, especially given that the proposals that sought to ensure "never-leak" behavior all felt like they had undiscovered holes of their own, leading to a lot of ceremony for no actual benefit. > I'm referring specifically to try!
I'm also referring to `try!`. :) It's a personal goal of mine this year to advocate a language-level replacement (and enhancement) for improved error-handling ergonomics.I think of things like the leakpocalypse the same way I think of security advisories like the Go one from a few days ago. Yeah, it looks bad on paper, but the odds of being affected by it in practice are so incredibly low that it's not worth worrying about.
Agreed about the potential of Rust, it sidesteps most of these problems. Though it might introduce other problems of its own, only time will tell. I expect that Rust's future sore spots will include destructor leaks, the reliance on macros, and the tremendous complexity of traits. (Though of course I don't claim to have better solutions to those.)