back

by vardump·12y ago·view on hn ↗
Golang has defer. http://golang.org/doc/effective_go.html#defer

That's what grandparent post should have been using.

  f, err := os.Open(filename)
  if err != nil {
     return fmt.Errorf("Failed to open config file: %s", err)
  }
  defer f.Close() // This is going to be always executed when leaving this function
  err = callFunctionThatMightErrorOut()
  ...
When writing C++, I wish I had defer over RAII. In C++, when interfacing with lower levels or C code, you need to first wrap everything in a class. Just so that you can use unique_ptr or shared_ptr etc. Or worse, write a scoping class. Ugh. Ugly and so many lines written for nothing...
2 comments
The downside to defer over RAII, of course, is that there's no static guarantee that you 1) remembered to include the defer, or 2) deferred the right thing. I could see that getting painful in some refactorings. Which isn't to say there aren't upsides.
Whenever you do forget, it's immediately obvious by looking at the function in question. Or by grepping things that "open" something to see if there's corresponding defer nearby.

In practise, it's not a big problem, because you learn fairly soon when doing something reversible that a defer is needed to reverse it when leaving scope/function. Like:

  func DoSomethingInSomeDir() error
  {
    err := os.Chdir("somedir")
    if err != nil { return err }
    defer os.Chdir("..") // always done even if there's panic
    ... do the something that can fail
    return nil
  }
Granted, using chdir in the first place is rather evil in anything but in a simple utility. And yes, chdir("..") doesn't necessarily bring us back to the starting position. And it itself can have error, which should be handled in real code. But this is a quick example. :-)

But I didn't need to write a ChdirToSomewhereAndBack wrapper class either. Control flow is completely obvious. All potential bugs are in plain sight, not hidden in some wrapper.

If you saw this code for the first time ever, you'd have absolutely no surprises or need to browse code in some class.

If you needed to do this chdir thing often, you could simply wrap it in a function:

  func DoSomethingInANamedDir(dirName string, fn() error) error
  {
    err := os.Chdir(dirName)
    if err != nil { return err }
    defer os.Chdir("..") // always done even if there's panic
    return fn(); // do the something that can fail
  }
then just:

  DoSomethingingInANamedDir("something", 
    func() err { ... something that can fail ... })
Of course, as someone who sees this for the first time needs to look inside DoSomethingingInANamedDir. Such is the price of wrappers.
'Whenever you do forget, it's immediately obvious by looking at the function in question. Or by grepping things that "open" something to see if there's corresponding defer nearby.'

That's not great. Static guarantees are worlds better than "I can manually look at it, and manually grep for places I need to look".

"But I didn't need to write a ChdirToSomewhereAndBack wrapper class either."

That's not exactly hard, and if you're doing something frequently then writing something to abstract it away is the right approach. It's true that many languages leave defining that wrapper class ugly and push it far away from the site of use in one-offs.

"All potential bugs are in plain sight, not hidden in some wrapper."

That would be one of the upsides, yes. The related downsides are that there is more room for bugs because you have to get it right every time instead of just once, and bugs might be hidden in plain sight when there is too much clutter.

"If you needed to do this chdir thing often, you could simply wrap it in a function:"

That seems isomorphic to the RAII wrapper class, with slightly more syntactic cruft.

"That's not great. Static guarantees are worlds better than "I can manually look at it, and manually grep for places I need to look"."

Grepping or looking is pretty much what you need to do also in C++, if you happen to have the bug in the wrapper. There's no static guarantee that can figure out if there's no chdir back to original location.

If you have a bug in the wrapper, you fix it once in the wrapper, and it's more likely to have shown up in a test if that wrapper is used multiple places. There is no static guarantee that you wrote the wrapper right, but hopefully there is some guarantee that you used the wrapper right. Definitions can't outnumber uses (or errors in some of those definitions don't matter...).
I wasn't saying "Therefore, C++ is better than go", just that the code above was unnecessarily ugly. I actually think that exceptional conditions are usually better handled with return values, though ideally in a way that allows some constrained bubbling without too much boiler plate plumbing. Of languages I'm deeply familiar with, I think Haskell has the best facilities for this (mark it in the type system, hide it behind composition, require it's been handled in controlled places) but has facilities for crazy stuff too (some of which winds up unavoidable).
Haskell's type system does sound interesting. Been on my list to check out for a long time, but so little time.

I write C++ for living. About a year ago, I wrote one pet project in Go. 30k lines, complicated concurrency, two different TCP servers, thousands of simultaneous goroutines that send messages over channels to each other. I was shocked how good experience it has been from the start when it comes to error handling and general reliability. Despite being my first and only project in Go, the project just had pretty much no bugs.

We've used this project daily by handful of people for over half a year and so far just a few minor bugs have been reported.

I've never achieved that low initial defect count for similar size and complexity C++ projects.

Happy to hear about your good experience :)