4 ms·
The problem with RAII is that it is semantically coupled to destruction in most (all?) of the languages it is implemented in, which is tied to lexical scope. I
by duped 4y ago
The problem with RAII is that it is semantically coupled to destruction in most (all?) of the languages it is implemented in, which is tied to lexical scope.
In GC'd languages where lifetime is not meaningfully coupled to scope, RAII cannot work the way you expect it to. The most trivial way to handle it is to couple to finalizers, which is tricky for two reasons: firstly it is not deterministic when (or if, in case of an exception/fatal error) the finalizer runs, and secondly it means the GC must keep track of user defined finalizers to execute on collection (what if they trigger an error? What if they take a long time? What if they block?).
The solution is to find some way to tie RAII to lifetimes, which may require exit analysis and can be extremely difficult, if not impossible, to implement in the language. The "least bad" way to deal with the problem is defer-like semantics.
I'm not a huge fan of defer, but it's important to understand why it exists. It's hardly "wrong by default" and more "understandably different."
- kevincox 4y ago> which is tied to lexical scope This isn't really true. It is kinda true in C++ but fairly well fixed with moves. In Rust it isn't true at all because moves are the default so any time you return a value or put it in a data structure the lifetime is no longer lexically bound. > It's hardly "wrong by default" and more "understandably different." I think it is both. What I mean by "wrong by default" is that if you write f = open(name) The code is now wrong. This is the default behaviour. To make the code corred you need to add a second bit of code (a close call). Defer is a powerful mechanism to do that, but it is a tool to solve the core wrongness.
- masklinn 4y agoNote that they said the destruction is tied to lexical scope, in general, not necessarily the lexical scope of creation. Their point is mostly that once you "drop" an object in a GC'd language, it will be collected at some later time independent of lexical scope. Which I don't think is much of an issue, technically the language could "drop" (close) such objects lexically and leave memory reclamation for later. Not a huge issue. The bigger issue is that GC-based objects have a strong tendency to share, a lot. That makes RAII way more challenging, when using the objects directly for starters, but even more so when composing RAII objects and non-RAII objects.
- marcosdumay 4y agoOn both C++ and Rust the destruction is tied to the value's lifetime. On the two languages that everybody remember when they talk about RAII, the destruction is not tied to lexical scope. (Although, on C++ that requires extra code for your class to behave correctly. The resource managing class is wrong by default, but the code that uses it is not.)
- tialaramex 4y agoIn Rust what you wrote is just how it works because it's how move was defined in the language, but I think in C++ at most you could say that this is how people conceptually try to arrange for things to happen, and at that point we're back to "Wrong by Default". All the C++ compiler actually gives you is destruction at end of lexical scope, and then it's your job as programmer to do what people actually wanted instead. Historically what you do in C++ is copy things a lot, or work with pointers. Modern C++ offers a move, but for backwards compatibility reasons it had to preserve that behaviour where the destruction happens precisely when you leave scope, so, move "hollows out" objects, leaving something which can be destroyed later. Thus it's your job, when implementing a C++ type, to ensure that the "real" work happens whenever a programmer would expect, leaving just some shell to be properly "destroyed" later when scope ends. Whereas for Rust you just implement Drop correctly and the compiler arranges for it to happen promptly.
- MauranKilom 4y ago> All the C++ compiler actually gives you is destruction at end of lexical scope, and then it's your job as programmer to do what people actually wanted instead. `new` and `delete` are built into the language (and smart pointers into the standard library). The statement that object lifetime is tied to lexical scope is plainly untrue. > Thus it's your job, when implementing a C++ type, to ensure that the "real" work happens whenever a programmer would expect The auto-generated (`= default`) copy and move constructors do the right thing by default if you compose your type from other (correct) types. Yes, in the rare case that you write a type that has to implement RAII, the compiler can do the wrong thing by default ("copy just copies the pointer"), but then you write a wrapper class once and you're done. If that's even necessary at all, since unique_ptr can have custom deleters. (You wouldn't violate Single Responsibility by messing with RAII in a class that also does other things, right?)
- ComputerGuru 4y agoThis is only technically true, in practice GC languages have workarounds. C# is almost entirely GC but has the IDisposable pattern and using statements to make up for this.
- cryptonector 4y agoAlso, `defer` is a patch applied to lame languages like C. It's fine as such -- the problem is the language, and that can't be fixed because the installed base is large.
- thayne 4y agoGo and zig have had defer pretty much from the beginning.
- thayne 4y agoEven in GC languages, you almost always want to free the resource at the end of a lexical scope, usually the same one where it was acquired. What if instead of requiring special syntax (defer, try-with-reaource, with etc.) for the normal case where you want it freed at the end of scope, you use special syntax to specify it shouldn't be automatically cleaned up, because it is persisted somewhere else?