Hacker News new | ask | show | jobs
by mskslal 2017 days ago
In its current form, this is really stupid, because of something like this:

    guard {
        for (int i = 0; i < n; i++) {
            defer foo(i);
        }
    }
Now the compiler has to:

1. implement some side of capture/closure mechanism to keep all the 'i's to the end of the guard block

2. do dynamic allocation to store the closures so they can be executed at the end did the scope

1 seems like too much work for such a feature, and 2 is a massive no. Implicit dynamic allocation, in C?

And all of this for nothing. The guard syntax doesn't give any reasonable benefits. They should have just kept it simple; defer happens at the end of the scope, and it just takes 'i' by "reference". It's a shame because it's a feature I would really like to have.

8 comments

Based on committee discussion, I think it is unlikely we will attempt to capture the values. The capture will most likely be done by reference.

This case of the defer in the loop is frequently cited, probably because it is a problematic case. However, I looked at a lot of real code and the only case I found of resources being allocated in a loop they were allocated at the beginning of the loop and deallocated at the end. Another option we are considering is to use the scope for the guarded block. In this case, deferred statements would be executed at the end of each iteration of the for loop which would be ideal for this sort of code. For example, you could rewrite this function using defer:

https://github.com/openssl/openssl/blob/a829b735b645516041b5...

like this:

    for (;;) {
        raw = 0;
        ptype = 0;
        i = PEM_read_bio(bp, &name, &header, &data, &len);
        defer {
          OPENSSL_free(name);
          name = NULL;
          OPENSSL_free(header);
          header = NULL;
          OPENSSL_free(data);
          data = NULL;
        }
 ...
        } else {
            /* unknown */
        }
    } // end for loop, run deferred statements
Agreed. In Go, defers accumulate and are all called at the end of a function, which I believe is a mistake (but there are practical uses for it). I think it would be much more fitting for C just to have the defer happen at the end of the scope. It would be very easy for compiler devs to implement it too--that above example could be done just by taking the defer block and pasting it at the end of the scope, with zero overhead. More complex control flow can be implemented with a simple goto.
The fact that the loop case is rare does not mean it isn't still problematic. It's creating a new opportunity to shoot yourself in the foot while trying to solve another one. (This is with the separate guard{} block.)

I would strongly agree that the dedicated guard{} block is a bad idea and this should just tie into the innermost scope. I see what this is trying to do ("if (...) { foo.x = malloc(); defer free(foo.x); }") but you don't solve a UI problem (as in, user interface for the programmer to their code) by adding more weird UI.

("Worst" example for shooting yourself in the foot: defer inside macros. Programmer then forgets that the macro contains a defer, and the defer defaults to the function-implicit outer guard block. But it's really in a nested loop. That's gonna be a fun week of debugging... much less of a risk when you have the guarantee in terms of innermost scope.)

Closures capturing iteration variables by reference is a common footgun in many languages unfortunately.
The simple solution to me seems to be to just ban use of loops hierarchically between a defer statement and a guard block. The guard block should definitely be kept as using scope seems like it confuses and complicates the notion of scope; for instance, it seems like a footgun if "if(some condition that turns out to be always true) {blah blah}" can't be refactored into "blah blah". Also, using scope seems to make it difficult to use defer in macros.
That is effectively one of the options that is under discussion. One of the advantage of the approach with defer is really that everything happens in the open and all uses that one would want to classify as misuse can easily be flagged by the compiler.
I got the impression that the proposal still has some aspects of the design that are open for discussion, including whether it should be static or dynamic, and whether it should capture the variables by value or by reference. (These are discussed in pages 13-16)
Based on feedback from the committee, I think it's very unlikely a dynamic approach will be used. The static only affects the for loop in terms of whether the deferred statements are run once or for each iteration of the loop, and the only real world use of this I've found so far involves allocating and deallocating resources within each loop iteration which would be supported by the static approach.
Yes, see the discussion in the proposal on page 13, under the heading "Should defer statements be static or dynamic?": http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2542.pdf
They can avoid #2 by doing by-reference capture or they can piggy-back on alloca().

Still, I agree that this is not "nice and clean". Ultimately it doesn't feel like something that belongs in C.

Doing defer in a loop is usually a bug in your Go code. Sure there are legit use cases of it, but in ~90% of the time what you really want to do is (in Go, I haven't used C recently so not sure how to do lambdas correctly):

    for i := 0; i < 10; i++ {
      func(i int) {
        defer foo(i)
        // other code
      }(i)
    }
No, actually none of that. That code has a constraint violation: it uses the variable i outside of its scope. Even if i would be declared on the same level as the guard, this sequence of deferred statements would not do good, but also not much harm. It would call foo with all the same value for i, namely n.

To have a good example where defer inside a loop actually does something, you'd have to capture the value of i. We also have a macro for that in the reference implementation, but that has a much more specialized scope of use.

This isn't really true. If you're capturing by reference, you can easily say that defer can only be called on variables declared in scopes no deeper than the beginning of the defer block and you still have a very useful feature. If you're capturing by value, the "closures" are probably just getting created in the stack like normal auto variables. Either way, this doesn't really seem to be a deal breaker.
I suspect it will come with some really big caveats. For instance what you wrote shouldn't compile, because i's storage duration doesn't extend to the end of the guard block. If you write

    guard {
        int i;
        for (i=0; i<n; i++) defer foo(i);
    }
I would expect foo(n) to be called n times. That is, variables won't be captured, it would work literally as if you wrote "foo(i)" n times at the end of the guard block. It's a footgun, but everything in C is a footgun and this behaviour is not surprising to me as a C developer.

So I expect the implementation would be

1. If a defer block references variables which do not live to the end of the guard block, the program is malformed (compiler error).

2. Compile each defer block as if it was written at the end of the guard block. What order the defer blocks are stored in is implementation-defined

3. Put a pointer on the stack each time a defer statement executes pointing to the compiled defer statement.

With this each defer statement is just a few bytes of overhead added to the stack and you can do it in a loop, though it may not do what you expect if you come from Go. For the example I expect the compiler to emit code similar to:

    struct defer_node {
        void **label;
        struct defer_node *next;
    };
    struct defer_node *defer_start=0;
    int i;
    for (i = 0; i < n; i++) {
        struct defer_start *defer_new = alloca(sizeof(struct defer_node));
        defer_new->label = &defer_stmt0;
        defer_new->next = defer_start;
        defer_start = defer_new;
    }
    
    while (defer_start) {
        goto *defer_start->label;
    defer_stmt_exit:
        defer_start = defer_start->next;
    }

    return;
    /* or break; or continue; whatever is appropriate for the enclosing block */

    defer_stmt0:
    foo(i);
    goto defer_stmt_exit;
This makes defer more or less just a mechanical transformation of code that can be expressed with existing C primitives and without requiring dynamic memory. I think it would be a good addition to C's structured programming elements.
alloca() is not a standard C primitive. It's only an extension, and its purpose is to dynamically allocate memory that is automatically freed. Using a dynamic amount of stack space is dynamic allocation, it's just not on the heap.

This still makes it really easy to overflow your stack, especially if for example your loop count above can come from user input. This is dangerous for all the same reasons that variable-length arrays are dangerous.

The proposal poses object value capture as an open question. See heading "Should object values be captured?" on page 14 of the proposal: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2542.pdf
Yes, you are correct and there wasn't much support for capturing values so we would probably only support capture by reference and wait to see if C solves this problem in general by introducing lambdas.
> What order the defer blocks are stored in is implementation-defined.

But the order they're executed in would have to be defined, lest we build ourselves another major bug generator.

Some commenters are claiming this issue can be worked around by using by-reference capture, but the following code still requires dynamic allocation:

  void reverse_bitstream(void * read_ctx, void * write_ctx) {
    guard {
      while(has_more_bits(read_ctx)) {
        int b = read_bit(read_ctx);
        if     (b == 0) defer write_bit(write_ctx, 0);
        else if(b == 1) defer write_bit(write_ctx, 1);
      }
    }
  }
An example like that is exactly why, in its current form, it's a bad idea. It's too high level. On the surface it seems like it makes code more consise and elegant, but in reality it just pushes the "ugly" implementation details and allocations onto the compiler. When you start writing code that's phrased in terms of high-level compiler features, rather than in terms of what the computer needs to do, then you're no longer writing C.