And leave memory leaks, buffer overflows, and bugs in the process, and we've done for the past 40+ years...
And leave memory leaks, buffer overflows, and bugs in the process, and we've done for the past 40+ years...
Yes, which is a much better formulation.
So it adds a new way to misinterpret the code: is cleanup deferred or not?
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.
Language features should be orthogonal. A new language feature should add something that is not possible or extremely painful to do with the existing language features. I just don't see how these minor syntax adjustments warrant a new feature, especially one with as much complexity and corner cases as this defer proposal.
(The real answer to "why defer?", of course, is that the authors need it to implement panic/recover. This proposal should stop masquerading as a defer mechanism for C and instead call itself what it really is: exceptions for C.)
My, I didn't think it was possible to miss the point like that. Are you even arguing in good faith? Let's examine for a moment the 3 other alternatives.
First, we get the "repeat ourselves" problem: when I have several exit points, I must clean up at each exit. And if I edit the code in any way, (for instance by adding yet another check), I must review everything that has been initialised until this point and clean it up there again. This might be okay if I have only 1 or 2 exit points, but if I have more this is clearly unacceptable.
Second, we have goto. We replace our exit points by a goto cleanup. That one at least can scale. I don't like it however for three reasons. First, the cleanup code is at the end, far from the init code, so checking that the two pairs together correctly is inconvenient. Second, I need to manage an additional variable for the return value. Third, goto is banned in a lot of places, no matter how convoluted the alternatives may be.
Third, we have this monstrous pyramid if else that wastes horizontal space, requires you to re-indent everything at the slightest edit, separates cleanup code from init code, and is just plain ugly. The only thing going for it is the single exit point, and frankly it isn't much.
---
Those "alternatives" are anything but. They're what we have to do when faced with a limited language that doesn't express what we want to say. Workarounds, not solutions.
> minor syntax adjustments
Your perspective must be seriously warped if you're calling the function-wide reorganisation I spoke of "minor syntax adjustments". Or you're not arguing in good faith.
> The real answer to "why defer?", of course, is that the authors need it to implement panic/recover.
That is a separate point, which I think I agree with. Me, I just want a way to trigger an instruction when we exit the current scope. It's the necessary complement to `break` and `return`, which provide ways to exit scope before the end of the block. We could get rid of them, and apply a straightjacket structured programming discipline of course, but personally, I don't think I'm ready to give up on `break` and `return`.
Clearly we disagree on whether a syntax change is minor. But first let me repeat the point I made that you ignored in between your accusations: it really is just syntax. Of the four examples in the second part of your post, if we assume defer is implemented like attribute cleanup and fix up the compile errors, your first and fourth example compile to the identical assembly code:
I would argue that the best solution is one you didn't present: move the "business logic" into a separate function, one that takes the necessary resources as arguments. This way you're no longer mixing up resource acquisition error handling with business logic, and the function that acquires the resources can use the nested if statement style (or any other style) with no downsides. No surprises here, it again compiles to the identical assembly code:
In my opinion the nested if style is better than using defer because it's completely linear with no backward jumps. But even if you disagree you can hardly complain about cleanup code being far from init code because the whole resource handling function is less than 20 lines of code regardless of what cleanup style you chose. It doesn't matter, which is why I argue that it's a minor syntax change not worthy of addition to C.
It's really not. When the impact of "syntax" are non-local like that, it's more than syntax. A compiler would handle this beyond the parsing stage. At the very least, it would seriously massage the AST to remove `defer` from it.
> if we assume defer is implemented like attribute cleanup and fix up the compile errors, your first and fourth example compile to the identical assembly code:
This is to be expected: they ultimately do the same thing, and optimisers are known to do significant, non-local transformations to the code.
> move the "business logic" into a separate function, one that takes the necessary resources as arguments.
So now I have a function with (likely) too many arguments, that's used only once, and my eyes have to jump around to get to it (or I have to reach for the F2 key). The pyramid may be more visible, but that's a meagre advantage.
> In my opinion the nested if style is better than using defer because it's completely linear with no backward jumps.
Not even a criterion in my book. I suspect you're having an overly operational mindset. A mindset I suspect has held the whole field back a couple decades. Don't think of it like a backward jump. It's meant to be viewed as deferred execution, triggered by scope exit.
> you can hardly complain about cleanup code being far from init code because the whole resource handling function is less than 20 lines of code
That was an example, dummy. In real code, I'd have more than 3 things to initialise, and their initialisation might not be as trivial (or as repetitive) as what I've shown here. That's when I really want to read the code from top to bottom, with concerns packed together. Defer/cleanup lets me do that. The other solutions, less so.
Look at it from the opposite direction: if x->y didn't exist today, and the billions of lines of existing C code all used (*x).y, would you support a proposal to add a new x->y operator to the language? I doubt it.
Do you not like the array subscript operator, either, since a[b] can be *(a+b)? How about a && b, you can replace that with (!!a) & (!!b), with an extra 'if' if you need the short circuiting.
Well, that's the whole point of syntactic sugar.
Not that it gives you something you can't already do, but that it gives you a succint and better way to do it.
>Language features should be orthogonal. A new language feature should add something that is not possible or extremely painful to do with the existing language features.
I beg to differ, based on your definition of "extremely painful". Many kinds of syntactic sugar are welcome, even when the previous native solution wasn't "extremely painful" but e.g. just tedious or error prone.
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.