Live data from Hacker News

A Defer Mechanism for C

gustedt.wordpress.com

101–110 of 248 posts

Re: A Defer Mechanism for C

#101
post #91
post #47

Earlier quoted context omitted.

It shouldn't be. std::unique_ptr is very clear. (std::shared_ptr should be used only when absolutely required and should be kept under close watch the whole time.)

searches codebase I'm working on... finds thousands and thousands of std::shared_ptr (many involve a typedef, so there are even more...) eep! Should I be alarmed?

Yes. On reflection, or on ramping up new developers, I think you’ll find that it is unnecessarily difficult to reason about object lifetimes.

There’s a good chance that most of the shared_ptr’s can be replaced by unique_ptr, which has less runtime overhead. More importantly, it documents the intent of the programmer regarding ownership semantics, and the timespan during which the object should be remain valid.

Re: A Defer Mechanism for C

#102
post #91
post #47

Earlier quoted context omitted.

It shouldn't be. std::unique_ptr is very clear. (std::shared_ptr should be used only when absolutely required and should be kept under close watch the whole time.)

searches codebase I'm working on... finds thousands and thousands of std::shared_ptr (many involve a typedef, so there are even more...) eep! Should I be alarmed?

If that codebase is older than C++0x, many of these std::shared_ptr could be taking the place of std::unique_ptr (which didn't exist back then; std::auto_ptr was a footgun). If you're in the mood for a refactoring, you could take a look at each one and see whether then can be replaced by std::unique_ptr, or whether they really have shared ownership semantics (which is what std::shared_ptr should be used for).

Re: A Defer Mechanism for C

#103
post #18

Earlier quoted context omitted.

> Or you know... free the things you need to free and move on with your life And leave memory leaks, buffer overflows, and bugs in the process, and we've done for the past 40+ years...

Sure. But defer just changes the problem from forgetting to write free to forgetting to write defer. So we’re not really talking about provably correct solutions... just things that can work. And I think a context or just manually freeing can work nicely.

> defer just changes the problem from forgetting to write free to forgetting to write defer.

That's the case only the first time you write code:

  {
      foo *f = new_foo();  // step 1
      /* lots of code */   // step 3
      free(f);             // step 2
  }
vs

  {
      foo *f = new_foo();  // step 1
      defer free(f);       // step 2
      /* lots of code */   // step 3
  }
Sure, in both cases you can forget step 2. But what about review? With defer, the init and cleanup code are besides each other, and a missing defer would be immediately suspicious. Without defer, you'd have to check the end of the block to make sure the cleanup code is there. The absence of the cleanup code wouldn't jump to your eyes the same way the absence of defer would. In the long run, this makes defer significantly harder to forget.

---

Another significant advantage of defer is that it can handle several exit points. Imagine this code:

  Foo f = new_foo();
  defer free(f);
  if (!f) {
      return FAIL_FOO;
  }
  Bar b = new_bar();
  defer free(b);
  if (!b) {
      return FAIL_BAR;
  }
  Baz z = new_baz();
  defer free(z);
  if (!z) {
      return FAIL_BAZ;
  }
  /* business logic */
  /* business logic */
  /* business logic */
  return SUCCESS;
Now the same, without defer:

  Foo f = new_foo();
  if (!f) {
      free(f);
      return FAIL_FOO;
  }
  Bar b = new_bar();
  if (!b) {
      free(f);
      free(b);
      return FAIL_BAR;
  }
  Baz z = new_baz();
  if (!z) {
      free(f);
      free(b);
      free(z);
      return FAIL_BAZ;
  }
  /* business logic */
  /* business logic */
  /* business logic */
  free(f);
  free(b);
  free(z);
  return SUCCESS;
You really don't want to repeat yourself like that, you'd be liable to forget something. Now we could use `goto` and a return value:

      ReturnValue retval = SUCCESS;
      Foo f = new_foo();
      if (!f) {
          retval = FAIL_FOO;
          goto cleanup;
      }
      Bar b = new_bar();
      if (!b) {
          retval FAIL_BAR;
          goto cleanup;
      }
      Baz z = new_baz();
      if (!z) {
          retval FAIL_BAZ;
          goto cleanup;
      }
      /* business logic */
      /* business logic */
      /* business logic */
  cleanup:
      free(f);
      free(b);
      free(z);
      return retval;
Better, except maybe the fact that Q/A hates you. All is not lost, you can still please them with a single exit point (pattern seen in the real world):

  ReturnValue retval = SUCCESS;
  Foo f = new_foo();
  if (f) {
      Bar b = new_bar();
      if (b) {
          Baz z = new_baz();
          if (z) {
              /* business logic */
              /* business logic */
              /* business logic */
          } else {
              retval FAIL_BAZ;
          }
          free(z);
      } else {
          retval FAIL_BAR;
      }
      free(b);
   } else {
      retval = FAIL_FOO;
  }
  free(f);
  return retval;
To be honest this may be the worst of them all.

---

The only real contenders for this use case are defer and goto, and even then I think I prefer defer.

Re: A Defer Mechanism for C

#105

GCC has a similar extension for defering to end of scope, which is basically most of use cases(cleanup function is usually free or some destructor/sanity check function). https://echorand.me/site/notes/articles/c_cleanup/cleanup_at...

This is amazing, thanks! It's a bit different though - it executes a cleanup function when the variable goes out of scope, rather than a statement at the end of the enclosing guard block. It might be better though - it avoids some footguns that would be possible with a defer implementation.

Re: A Defer Mechanism for C

#106
post #23
post #6

Earlier quoted context omitted.

> One of the key interest of C compared to more recent languages is that everything is explicit. There's nothing implicit about defer.

Deferred actions are not explicit at the exit points. That's quite implicit. You can no longer reason about a local piece of code; you now have to know its lexical nesting up to top level to see if it's inside a guard block that might trigger hidden behavior.

It's as much hidden behaviour as destructors being called magically on an object that goes out of scope. In practice it doesn't hinder understandability as much as you think.

Re: A Defer Mechanism for C

#107

Earlier quoted context omitted.

I would strongly disagree on RAII being a kludge in C++. The language feature that enables it is deterministic destructors, whose primary purpose is to ensure that allocated resources are deallocated. RAII is one particular use of destructors. The primary goal is to have the object be the thing that owns a resource, not the calling scope. In terms of usability, destructors allow for resource management that is far ea…

I do feel that RAII is a little bit ruined by the fact that you cannot declare anonymous object instances. For example, I cannot write: lock (mutex); Expecting to declare an anonymous lock with the mutex passed in, as this gets parsed as a declaration of a lock called mutex (hopefully lock doesn’t have a default constrictor and I at least get an error). Instead I have to bake my lock: lock l(mutex); Often I have obje…

IIRC, the problem with C++ is that you can declare anonymous object instances... With surprising results (the object is destroyed at the semicolon, instead of being destroyed at the end of the block).

Re: A Defer Mechanism for C

#108
post #18

Earlier quoted context omitted.

> Or you know... free the things you need to free and move on with your life And leave memory leaks, buffer overflows, and bugs in the process, and we've done for the past 40+ years...

Sure. But defer just changes the problem from forgetting to write free to forgetting to write defer. So we’re not really talking about provably correct solutions... just things that can work. And I think a context or just manually freeing can work nicely.

The main use case is not forgetting to free resources. It's when you do have resource-freeing code, but it's tricky to make sure you free only the resources you know for sure you have already allocated. This is common in the Linux kernel. A great example is this function I found at random from fork.c: https://github.com/torvalds/linux/blob/master/kernel/fork.c#...

Notice that there are five different goto targets, each for a specific case of what has-and-has-not been allocated. The resources are memory, locks, and even TLB flushing. This code would probably be cleaner with defer.

Re: A Defer Mechanism for C

#109

Earlier quoted context omitted.

I would strongly disagree on RAII being a kludge in C++. The language feature that enables it is deterministic destructors, whose primary purpose is to ensure that allocated resources are deallocated. RAII is one particular use of destructors. The primary goal is to have the object be the thing that owns a resource, not the calling scope. In terms of usability, destructors allow for resource management that is far ea…

I do feel that RAII is a little bit ruined by the fact that you cannot declare anonymous object instances. For example, I cannot write: lock (mutex); Expecting to declare an anonymous lock with the mutex passed in, as this gets parsed as a declaration of a lock called mutex (hopefully lock doesn’t have a default constrictor and I at least get an error). Instead I have to bake my lock: lock l(mutex); Often I have obje…

it is a bit annoying yes. There are some proposals to have a anonymous reusable placeholders like '_', but they haven't gone anywhere yet.

You can do

    lock(mutex), some-lock-protected-expression; 
but it is a bit too cute and limited.

On the other hand, being able to name the RAII object is often very useful for example if you need to dismiss them early, which happens often in transactional code (unlocking a mutex before the end of scope is a relatively common occurrence).

Also specifically for mutexes, a nice pattern is to bind object and mutex together to guarantee that the object is always used with the lock:

   sync foo;

   {
     auto l = foo.lock(); // starts the critical section

     l->do_something()
     l->do_something_else();
   } // it ends here

   // or if you only need to hold the mutex for a single call:

   foo->do_something(); // critical section lives only for the full expression

Re: A Defer Mechanism for C

#110

void * * a = NULL; while(TRUE) { a = malloc(sizeof *a); if(a == NULL) break; *a = malloc(1024); if(*a == NULL) break; if(!do_some_other_tests(a)) break; return a; } /* cleanup / if(a == NULL) return; if( a != NULL) free(*a); free(a); alt: void * * a = NULL; switch(TRUE) { defaultt : a = malloc(sizeof *a); if(a == NULL) break; *a = malloc(1024); if(*a == NULL) break; if(!do_some_other_tests(a)) break; return a; } /* c…

If the proposed implementation requires a guard clause, then it seems like it is no better than the while (TRUE) implementation above and it's not worth the support in the standard. Maybe it's a bit more structured and covers a few more use-cases, but not worth the standardization.
Post reply on HN