5 ms·
In its current form, this is really stupid, because of something like this: guard { for (int i = 0; i < n; i++) { defer foo(i);
by mskslal 6y 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.
- ufo 6y agoI 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)
- scott_s 6y agoYes, 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 http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2542.pdf
- rseacord 6y agoBased 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.
- huhtenberg 6y agoThey 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.
- Asooka 6y agoI 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.
- shawxe 6y agoThis 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.
- fishywang 6y agoDoing 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) }
- rseacord 6y agoBased 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/a829b735b645516041b55746e013692babd8cd31/crypto/pem/pem_info.c#L52-L265 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
- mskslal 6y agoAgreed. 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.
- eqvinox 6y agoThe 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.)
- dooglius 6y agoSome 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); } } }
- mskslal 6y agoAn 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.
- gusttedt 6y agoNo, 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.