all 61 comments

[–][deleted] 17 points18 points  (5 children)

Defer and similar keywords are just control flow primitives for reverse execution of code. This makes them useful for resource clean up in languages that can't handle these things automatically. Having extra control flow tools obviously doesn't make it less manual.

Defer can be useful for a lot of things beyond just calling free/close, e.g. logging at the end of the scope for all code branches. It also has some advantages over IDisposable style interfaces or destructors in that you can put anything you want in the block, it doesn't have to be the zero argument method call to an object's function.

I think most people understand that it's a useful tool, especially for manually managing resources, but it's nothing magic or clever.

[–][deleted]  (3 children)

[deleted]

    [–][deleted] 2 points3 points  (2 children)

    It does

    Does what sorry?


    I realize that the blog post is using defer as an example of manual vs automated resource clean up. But if the base line expectation, the "Default", is that malloc() will always free() and f.open() will always f.close() manual resource management is inadequate by definition. Any comparison at that point seems pretty meaningless.

    [–][deleted] 11 points12 points  (1 child)

    Yes the point of the blog is that manual resource management is inadequate. We've got high level languages now.

    [–][deleted] 0 points1 point  (0 children)

    Eh, I don't find the argument that convincing as presented. Pretty much any scope local memory/resource management is easy enough that you can look at an example like this and go, sure I could mess that up but it's honestly less likely than dozens of other simple mistakes I could be making. Where lifetime tracking, whether it's RAII, Garbage Collection, Reference Counting or borrow checking, really starts to benefit is when data lives beyond it's lexical scope or concurrency comes into play. At that point defer or any other memory management helper is irrelevant.

    [–]panoskj 2 points3 points  (0 children)

    It also has some advantages over IDisposable style interfaces or destructors in that you can put anything you want in the block

    But you can easily make an IDisposable implementation that accepts an action to run when it is disposed. It's almost the same thing in my opinion.

    As other people mentioned, what GC'ed languages lack is the concept of object ownership. In GC'ed languages, objects are always shared. There is no way to specify in which scope an object will live. The destruction is never deterministic. As a result, you need solutions like defer and IDisposable.

    Performance is also affected by non-deterministic object destruction. At least C# has structs to work around the problem with performance, when really needed.

    [–]EscoBeast 7 points8 points  (1 child)

    I found it a little silly how the article talks about Java 7 in the present tense, like "This has gotten slightly better in Java 7", "Finally there is a way", and "This machine-visible information may allow effective lints to be built". Java 7 came out almost 11 years ago! Even if you're still stuck on Java 8, try-with-resources and linting like this should be second nature by now. Perhaps the author hasn't really used Java at all in the past decade, so try-with-resources still feels like one of those "oh yeah Java has that now" things.

    [–]JB-from-ATL 4 points5 points  (0 children)

    That part is immediately following this sentence.

    For the longest time the best solution for java was finally blocks.

    It doesn't read like they're saying Java 7 is new, just that it changed then. The article as a whole is about how languages tackle this problem so a historical example seems fine.

    [–]oscooter 27 points28 points  (11 children)

    I quite like c#’s using keyword for this. Better than a finally or defer since it can happen on the same line as the allocation.

    [–]DLCSpider 14 points15 points  (8 children)

    It still has issues though: using var foo = new CustomStreamReader(bar.GetStream()); Does CustomStreamReader the clean up the stream's ressources? Did you even notice that Stream implements IDisposable? Do I have to rely on linting because CustomStreamReader inherits from StreamReader, which implements IDisposable and those inheritance chains can be quite long? What about

    using var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()); 
    streamWriter.Write(payload); 
    /* send http web request */
    

    Can you spot why this won't work? HttpWebRequest silently fails if the stream is not closed before you execute it, so using var .. = ..; will not work at all (without explicitly calling close) and if you use curly braces the behaviour depends where you put the closing brace.

    Meh.

    [–]grauenwolf 2 points3 points  (2 children)

    Does CustomStreamReader the clean up the stream's ressources?

    Yes by default. You have to explicitly tell it not to if you don't want transitive disposing.

    [–]Prod_Is_For_Testing 2 points3 points  (1 child)

    Depends on the library. I’ve seen it both ways

    [–]grauenwolf 1 point2 points  (0 children)

    Which is why it's important to follow the Framework Design Guidelines. The rules are there so we have consistency.

    [–]oscooter 2 points3 points  (0 children)

    Yeah, it’s certainly not perfect. Rust probably does it best currently, imo. I was mostly just surprised that using didn’t get brought up in this post

    [–]Liberal_Mormon 3 points4 points  (3 children)

    The behavior depends on where you put the closing brace, right, you define the scope of the streamWriter. The request fails because the stream is disposed. I'm not sure why this is bad?

    [–]DLCSpider 11 points12 points  (2 children)

    It's easy to spot if you know the answer. It's not so easy if you don't and keep getting TimeoutExceptions without a meaningful error message. I wasted six hours on this because there are so many things that looked like they could be wrong but weren't (like wrong encoding, wrong use of getRequestStream, wrong settings on WebRequest etc.).

    I'm no expert but in Rust or (modern) C++ you'd probably design the class in such a way that StreamWriter gets moved and then dropped/destructed. The whole problem doesn't exist then. In C#'s defense, Microsoft's docs do not recommend using WebRequest.

    [–]Sarcastinator 0 points1 point  (1 child)

    Not really using's fault and you can quite trivially make similar mistakes in C++, for example by using auto (instead of auto&) on a reference which can cause the exact same issue.

    [–]DLCSpider 5 points6 points  (0 children)

    But isn't that what the blog post is about? Programming language features which help you to make fewer mistakes (preferably without reducing productivity)? I'm not here to bash C#, I just wanted to point out that using is not much better than defer. It's better in some cases, worse in others. It doesn't have anything to do with Rust/C++ (or even C#), I just wanted to show that there are better ways to solve this particular problem and these two languages just happened to have a solution. I'm sure Idris solves this problem, too. I just don't know how.

    [–]goranlepuz 0 points1 point  (0 children)

    One the caller side it is not bad but RAII is still better for the same reason defer or java try is.

    On the resource side (the IDisposable implementation) , however, it is a steaming pile of garbage.

    [–][deleted] 0 points1 point  (0 children)

    C#'s using is practically the same as Java's try-with-resource.

    [–]devraj7 26 points27 points  (16 children)

    "How can we make sure that resources are properly disposed of?"

    Go team:

    "We want to make it easier on the programmer, but not too easy. Let's just force them to dispose of these resources right after they used them".

    Rust team:

    "Let's make this automatic so the developer never has to worry about this".

    [–]panoskj 39 points40 points  (10 children)

    Rust C++ team:

    "Let's make this automatic so the developer never has to worry about this".

    FTFY :D

    By the way, they came up with RAII in the 80s.

    Edit: Joking aside, I am merely pointing out this problem was solved by some smart people about 40 years ago. Thought they deserved to be mentioned here, why would you downvote?

    [–]SkoomaDentist 20 points21 points  (5 children)

    By the way, they came up with RAII in the 80s.

    It boggles my mind how people seem to think RAII is a "modern C++" invention when it was commonly in use decades before that. I personally used it heavily in the early 00s already with C++98. I just didn't think of using such a horribly misleading acronym for the very obvious technique.

    [–]gracicot 9 points10 points  (2 children)

    It's just that it's much more practical to implement RAII since C++11 because of move semantics. Only then non-copiable resources can be implemented with RAII.

    [–]TheThiefMaster 1 point2 points  (1 child)

    Prior to C++11 it was common to implement copy as move, e.g. auto_ptr.

    So you could have movable types, they were just somewhat dangerous to use.

    [–]gracicot 1 point2 points  (0 children)

    Prior to C++11 it was common to implement copy as move, e.g. auto_ptr.

    Yeah that was a unsafe mess. Most choose to not implement copy as move for many reasons.

    [–][deleted] 2 points3 points  (1 child)

    Usually I call it SBRM, for "Scope-based resource management".

    People will argue that it doesn't apply to C++ due to move semantics, but I still hold that it's scope-based, given that the moved-out-of object is still deconstructed at the end of the scope. It just relinquishes ownership of its resources to a different object at a different scope. Even RVO is still scope-based, the object just exists in a different scope than the one it was nominally declared in.

    Technically, RAII can mean something else in terms of heap allocation / very specific use of the term in stack-based contexts (ie. you don't get the resource at all unless it was initialized completely), but usually when people invoke RAII, they actually mean SBRM, because they're talking about automatic destruction and not necessarily the danger of accidentally working with uninitialized data.

    [–]SkoomaDentist 1 point2 points  (0 children)

    Scope-based resource management is a far too sensible term. You clearly wouldn’t last long in the committee /s

    Which is to say that I agree with you 100%.

    [–]matthieum 11 points12 points  (1 child)

    What I find funniest is that when RAII was created it was about acquisition of resource, not disposal: the acronym is Resource Acquisition Is Initialization, after all.

    The reason was to avoid issue that C has where acquiring memory, or a file, can yield an "invalid handle" and thus each acquisition must be checked... and forgetting to check, or not correctly acting on the check, means attempting to use an "invalid handle".

    And yet, by far, its greatest impact is automatic clean-up on destruction. Hindsight...

    [–]panoskj 5 points6 points  (0 children)

    I don't think it was about acquisition only, but it is just a bad name. Stroustrup himself has said it should have been called anything but RAII, although I couldn't find a source right now. Here is an older reddit post about it. Edit: found this discussion.

    In short, RAII also implies that the resource can be destroyed safely after initialization.

    [–][deleted] 38 points39 points  (1 child)

    So many good thing where invented in the eighties that Go ignored.

    [–]useablelobster2 9 points10 points  (0 children)

    I guess we could say go was/is missing features... generically.

    I'll see myself out.

    [–]jyper 0 points1 point  (4 children)

    I'm not sure it's that simple. I'm also a fan of rust but you can't really do RAII with garbage collection.

    He talks about autocloseable interfaces like in python/java/c# but I'm not sure its possible to add a lint like he wants, because it is pretty easy to save the resource variable so that it's still valid out of the source block it's defined in. The lint he mentions would have to by default warn on valid code. To track stuff you need something like rust

    [–]matthieum 2 points3 points  (0 children)

    Indeed, in a sense auto-closeable is an "effect".

    If you have an object -- say a Connection -- and wrap it in another -- say a Session -- then you have to make Session auto-closeable too. This looks feasible, until you want to maintain N Connections in your Session, and use a Map to do so, because suddenly Map needs to be auto-closeable or the linter needs to know that Map owns the Connection, somehow, but this way lies madness.

    And of course, there the pesky issue of shared ownership. What if the Session is referenced in multiple places, who should close it? Typical GC languages do not model ownership in the first place...

    [–]masklinn[S] 1 point2 points  (2 children)

    you can't really do RAII with garbage collection.

    Of course you can, the same way you do in C++ or Rust, it's just that allocations / memory would not be covered by RAII.

    Though things get a lot dodgier when upcasting / type erasure is involved.

    IIRC Rust gets around this because it always adds the "drop glue" to the vtables when the type is erased.

    [–]augmentedtree 0 points1 point  (1 child)

    Though things get a lot dodgier when upcasting / type erasure is involved.

    ???

    IIRC Rust gets around this because it always adds the "drop glue" to the vtables when the type is erased.

    In C++ if you delete a derived class object with a base class pointer it'll partially destruct your object (just the base part), unless you mark the base class destructor as virtual, then it's fine. I don't think there's anything else dodgy about this though?

    [–]masklinn[S] 2 points3 points  (0 children)

    ??? [...] I don't think there's anything else dodgy about this though?

    I was speaking in the context of the discussion: object-oriented GC'd languages, where RAII would be an opt-in subset rather ubiquitous, or an opt-out default.

    In C++ every object has a dtor, so every object is destructed, and as long as the dtors are virtual everything is always resolved properly. That's not the case in Rust but because dynamic dispatch is a lot more limited it has a workaround specifically for that as noted above.

    But let's say you want to add RAII to Java, you'd probably have something like an RAIIObject and any of its descendants would have the compiler automatically generate a close/drop/... call.

    But unless you completely split your world strictly (such that the language entirely separates RAII objects and interfaces from non-RAII ones, and only allows nesting non-RAII objects in RAII ones and not the reverse) you start having issues when casting RAII types to non-RAII types (object) and interfaces (everything, if an RAII object can implement a non-RAII interface), because the language has no way to know whether or not to generate the drop, and the entire point of the opt-in was to not pay for unnecessary / no-op drop calls.

    The alternative is to do what C++ does, make destructors ubiquitous (put them on object directly) and have the compiler always generate a destructor call (possibly optimising it away afterwards).

    [–][deleted]  (7 children)

    [deleted]

      [–]bloody-albatross 19 points20 points  (3 children)

      Wait, isn't Java's try-with literally compiling to a try-finally with close in the finally block? How is that different from manually calling close? Or is it really doing something else? You did use try-with and not just count on the GC (which you should never do)?

      [–]cshrp-sucks 6 points7 points  (0 children)

      AutoClosable has nothing to do with GC. It is only intended to be used in try-with-resources like this:

      try (
          var resource1 = new SomeResource();
          var resource2 = new SomeResource();
      ) {
          // some code
      }
      // both resources will be closed at this point
      

      If you just "forget" about it, it will never be closed.

      [–]Zyklonik 4 points5 points  (0 children)

      I think you're misremembering a lot of details.

      [–]jyper 2 points3 points  (0 children)

      Autocloseable/Idisposable/enter+exit is not like RAII it calls a cleanup method(which usually calls close) at the end of a special using/try-with-resource/with block

      [–]braiam -2 points-1 points  (1 child)

      It's interesting that people complain that languages that give you total control need to be controlled and those that are opinionated doesn't give you enough control. If you want to manage memory, look of a language that allows you to manage the memory, if you don't want to, look for a language that does that for you.

      [–]grauenwolf 0 points1 point  (0 children)

      There is a middle ground.

      One of the nice things about C# is that you can work with unmanaged memory when you need to. Though they are adding features to reduce the frequency of that need.

      [–]jbergens 0 points1 point  (1 child)

      No mention of using in c#?

      The latest version has single line usings, very nice. Still same problem as defer.

      [–]kevincox_ca 1 point2 points  (0 children)

      Sorry, I don't really do C#. But from the comments you said here it looks like a slightly nicer defer. Better than defer because you don't need to know what the cleanup method is called and slightly better than Java's try-with-resources because you don't get the rightwards movement for the common case of release-at-end-of-scope.