Live data from Hacker News

C: Simple Defer, Ready to Use

gustedt.wordpress.com

81–90 of 157 posts

Re: C: Simple Defer, Ready to Use

#82
I don't think "you can implement this slightly modified version of the proposal in GCC macros" is a good reason to change the defer proposal. It doesn't work in clang¹ or any other non-GCC C implementation, a trailing semicolon after the close brace is an ugly wart, and saying "if you want to implement this all you have to do is implement [[gnu::cleanup]] and nested local functions" is presumably much more work for other C implementations than just implementing defer is. I also question whether this even works in GCC with executable stacks disabled (I know GCC can optimize away the trampoline since the function doesn't escape the local scope, but does GCC let you write nested functions at all without executable stacks?).

¹The author says it works in clang using Apple's Blocks feature, but Blocks should not be required for defer and the variable semantics are wrong so it's a non-starter.

Re: C: Simple Defer, Ready to Use

#83
post #37
post #32

Earlier quoted context omitted.

The trampolines are not generated in this case.

To be fair you're not communicating very well — there are no trampolines being used here because the call site is directly in the same function, so no trampoline is needed with already being in the correct stack frame. (And also on -O1 and higher the entire call is optimized out and the nested function inlined instead.)

I feel that uecker went above and beyond the call of duty by including a godbolt link in their first comment which shows the full assembly-language implementation of this behavior by GCC without using an executable stack, with syntax highlighting, and full C source for reproducing the behavior on your own machine. I don't see how anything they could possibly have written as a comment could be clearer or more convincing.

Re: C: Simple Defer, Ready to Use

#84
post #76

I'm a strong believer that if C had defer, many bugs would disappear. The number 1 issue I find myself having when switching from C++ to C is missing RAII (Main C++ way of implementing defer).

The major source of bugs in C is located on string.h, and nothing has been made in 50 years to fix that. Really fix, not mitigations with their own gotchas.

There have been things done to try to fix that, see "strcpy_s" and other related functions. Visual Studio even considers use of the classic string functions (like "strcpy") to be a compiler error. You need to define a specific macro before you are allowed to use them.

Re: C: Simple Defer, Ready to Use

#85
post #27

Does C really need this? Do languages need to grow in this way? The overriding virtue of C is simplicity.

> Does C really need this?

Yes.

> Do languages need to grow in this way?

Yes.

> The overriding virtue of C is simplicity.

C is not simple. It used to be, but it's not been for a long time.

Re: C: Simple Defer, Ready to Use

#87

Earlier quoted context omitted.

Destructors are hidden control flow, and can be non-obvious for structs from other files. I find they make code significantly harder to follow than in plain C. Defer does not have the problem, as all the logic in your function is explicitly there.

Not right there, some other place in the function. Also people will start adding defer in macros and then things will go sideways.

> Not right there, some other place in the function.

Exactly. Both C++ RAII (constructors/destructors) and C23 defer are awful. They make the behavior implicit; in effect they tie an indefinitely large (and growing) baggage to scope termination without the source code reflecting that baggage.

Cascading gotos (or the arrow pattern) are much better. Cleanup code execution is clearly reflected by location in the source code; the exit path can be easily stepped through in a debugger.

Re: C: Simple Defer, Ready to Use

#88

I really, really do not like what that could do to the readability of code if it was used liberally. It's like GOTOs, but worse, because it's not as visible. C++'s destructors feel like a better/more explicit way to handle these sorts of problems.

> I really, really do not like what that could do to the readability of code if it was used liberally. > It's like GOTOs, but worse, because it's not as visible. > C++'s destructors feel like a better/more explicit way to handle these sorts of problems. But what C++ gives you is the same thing: > It's like GOTOs, but worse, because it's not as visible. ! The whole point of syntactic sugar is for the machinery to be h…

> The whole point of syntactic sugar is for the machinery to be hidden

And when the machinery fails, you'll not only have the machinery to debug, but the syntactic sugar too.

Re: C: Simple Defer, Ready to Use

#89
post #84
post #76

Earlier quoted context omitted.

The major source of bugs in C is located on string.h, and nothing has been made in 50 years to fix that. Really fix, not mitigations with their own gotchas.

There have been things done to try to fix that, see "strcpy_s" and other related functions. Visual Studio even considers use of the classic string functions (like "strcpy") to be a compiler error. You need to define a specific macro before you are allowed to use them.

The much dreaded Annex K functions are perhaps the worst possible example of an attempt at "fixing" anything safety related in C. A waste of ink.

Re: C: Simple Defer, Ready to Use

#90
post #58

I really, really do not like what that could do to the readability of code if it was used liberally. It's like GOTOs, but worse, because it's not as visible. C++'s destructors feel like a better/more explicit way to handle these sorts of problems.

How do you write C code that needs to do this (set up several resources, and clean only some of them up depending on where the function returns) so that it’s easy to follow?

When constructing an object in C, we may need some permanent sub-objects, and some temporary objects.

If we only need permanent sub-objects, then we set those up gradually, and build an error path in reverse order (with gotos or with the arrow pattern); upon success, the sub-objects' ownership is transferred to the new super-object, and the new super-object is returned, just before the error path is reached. Otherwise, the suffix of the error path that corresponds to the successful prefix of the construction path is executed (rollback). This approach cannot be rewritten with "defer" usefully. Normally you'd defer the rollback step for a sub-object immediately after its successful construction step, but this rollback step (= all rollback steps) will run upon successful exit too. So you need a separate flag for neutering all the deferred actions (all deferred actions will have to check the flag).

If we only need temporaries (and no permanent sub-objects), then (first without defer, again) we build the same code structure (using cascading gotos or the arrow pattern), but upon success, we don't return out of the middle of the function; instead, we store the new object outwards, and fall through to the rollback path. IOW, the rollback steps are used (and needed) for the successfully constructed temporaries regardless of function success or failure. This can be directly expressed with "defer"s. The problem is of course that the actual rollback execution order will not be visible in the source code; the compiler will organize it for you. I dislike that.

If we need both temporaries and permanent sub-objects, then we need the same method as with temporaries, except the rollback steps of the permanent sub-objects need to be restricted to the failure case of the function. This means that with either the cascading gotos or the arrow pattern, some teardown steps will be protected with "if"s, dependent on function return value. Not great, but still quite readable. With defer, you'll get a mix of deferred actions some of which are gated with ifs, and some others of which aren't. I find that terrible.

Post reply on HN