defer is cleaner, though.
The goto Dijkstra is talking about is dead. It lives only in assembler. Even BASIC doesn't have it anymore in any modern variant. Modern programmers do not need to live in fear of modern goto because of how awful it was literally 50 years ago.
(Free Hero Mesh uses setjmp in the execute_turn function. This function will call several other functions some of which are recursive, and sometimes an error occurs or WinLevel or LoseLevel occurs (even though these aren't errors), in which case it will have to return immediately. I did try to ensure that this use will not result in memory leaks or other problems; e.g. v_set_popup allocates a string and will not call longjmp while the string is allocated, until it has been assigned to a global variable (in which case the cleanup functions will handle this). Furthermore, the error messages are always static, so it is not necessary to handle the memory management of that either.)
Parent is correct that this doesn't really exist outside of assembly language anymore. There is no modern analogue, because Dijkstra's critique was so successful.
He should have said "correct code", not "modern code" because the times I remember seeing goto the code was horribly incorrect and unclear.
(With break and continue, someone has to be doing something extra funky to need goto. And even those were trigger signs to me, as often they were added as Hail Mary's to try to make something work)
{I typically reviewed for clarity, correctness, and consistency. In that order}
His paper [1] clearly talks about goto semantics that are still present in modern languages and not just unrestricted jmp instructions (that may take you from one function into the middle of another or some such). I'd urge everyone to give it a skim, it's very short and on point.
[1] https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.p...
I'm way less worried about uses of goto that are rigidly confined within some structured programming scope. As long as they stay confined to specific functions, they are literally orders of magnitude less consequential than the arbitrary goto, and being an engineer rather than an academic, I take note of such things.
I don't ask Alan Kay about the exact right way to do OO, I don't ask Fielding about the exact right way to do REST interfaces, and I don't necessarily sit here and worry about every last detail of what Dijkstra felt about structured programming. He may be smarter than me, but I've written a lot more code in structured paradigms than he ever did. (This is not a special claim to me; you almost certainly have too. You should not ignore your own experiences.)
The problem with GOTO, to Dijkstra, is that it violates that principle. A block can arbitrarily go somewhere else--in the same function, in a different function (which doesn't exist so much anymore)--and that makes it hard to reason about. Banning GOTO means you get the fully structured program that he needs.
(It's also worth remembering here that Dijkstra was writing in an era where describing algorithms via flowcharts was common place, and the use of if statements or loops was far from universal. In essence, this makes a lot of analysis of his letter difficult, because modern programmers just aren't exposed to the kind of code that Dijkstra was complaining about.)
Since that letter, modern programming has embraced the basic structured programming model--we think of code almost exclusively of if statements and loops. And, in many language, goto exists only in extremely restricted forms (break, continue, and return as anything other than the last statement of a function). It should be noted that Dijkstra's argument actually carries through to railing against the modern versions of break et al, but the general program of structured programming has accepted that "early return" is an acceptable deviation from the strictly-single-entry-single-exit that is desired that doesn't produce undue cognitive overhead. Even where mind-numbing goto exists today (e.g., C), it's similarly largely used in ways that are similar to "early return"-like concepts, not the flowchart-transcribed-to-code-with-goto-as-sole-control-flow style that Dijkstra is talking about.
And, personally, when I work with assembly or LLVM IR (which is really just portable assembly), I find that the number one thing I want to look at a very large listing is just something that converts all the conditional/unconditional jumps into if statements and loops. That's really the main useful thing I want from a decompiler; everything else as often as not just turns out to be more annoying to work with than the original assembly.
The modern goto is not the one he wrote about. It is tamed and fits into the structured programming paradigm. Thus, ranting about goto as if it is still the 1960s is a pointless waste of time. Moreover, even if it does let you occasionally violate structured programming in the highly restricted function... so what? Wrecking one function is no big deal, and generally used when it is desirable that a given function not be structured programming. Structured programming, as nice as it is, is not the only useful paradigm. In particular state machines and goto go together very nicely, where the "state machine" provides the binding paradigm for the function rather than structured programming. It is perhaps arguably the distinctive use case that it lives on for in modern languages.
No, it doesn't, not the goto of C or C++ (which tames it a smidge because it has to). That's the disconnect you have. It's not fine just because you can't go too crazy and smash other functions with it anymore. You can still go crazy and jump into the middle of scopes with uninitialized variables. You can still write irreducible loops with it, which I would argue ought to be grounds for the compiler to rm -rf your code for you.
There are tame versions of goto--we call them break, continue, and return. And when the C committee discussed adding labeled break, and people asked why it was necessary because it's just another flavor of goto, I made some quite voluminous defense of labeled break because it was a tame goto, and taming it adds more possibility.
And yes, the tame versions of goto violate Dijkstra's vision. But I also don't think that Dijkstra's vision is some sacrosanct thing that must be defended to the hilt--the tame versions are useful, and you still get most of the benefits of the vision if you have them.
In summary:
a) when Dijkstra was complaining about goto, he would have included the things we call break, continue, and early return as part of that complaint structure.
b) with the benefit of decades of experience, we can conclude that Dijkstra was only partially right, and there are tame goto-like constructs that can exist
c) the version of goto present today in C is still too untamed, and so Dijkstra's injunction against goto can apply to some uses of it (although, I will note, most actual uses of it are not something that would fall in that category.)
d) your analysis, by implying that it's only the cross-function insanity he was complaining about, is wrong in that implication.
It is difficult when speaking across languages, but in many cases, no, you can't.
https://go.dev/play/p/v8vljT91Rkr
C isn't a modern language by this standard, and to the extent that C++ maintains compatibility with it (smoothing over a lot of details of what that means), neither is it. Modern languages with goto do not generally let you skip into blocks or jump over initializations (depending on the degree to which it cares about them).
The more modern the language, generally the more thoroughly tamed the goto is.
BEGIN
INT x := 1;
print(("x is", x, newline));
GOTO later;
INT y := 2;
later:
print(("y is", y, newline))
END
for which we have: $ a68g goto.a68
x is +1
11 print(("y is", y, newline))
1
a68g: runtime error: 1: attempt to use an uninitialised REF INT value (detected in [] "SIMPLOUT" collateral-clause starting at "(" in this line).
Although admittedly it is a runtime error.However if y is changed to 'INT y = x + 2;', essentially a "constant", then there is no runtime error:
$ a68g goto.a68
x is +1
y is +0
(1) For simpler cases, wrap in do {} while (0) and break from the loop
(2) For multiple cleanups, use same technique combined with checks to see if the cleanup is required. E.g. if (f != null) fclose (f)
(3) put the rest of the stuff in another function so that the exit code must run on the way out.
In 35 years of coding C/C++, I've literally never resorted to goto. While convenient, this new defer command looks like the kind of accidental complexity that templates brought to C++. That is, it provides a simple feature meant to be solve simple problems in a simple way that accidentally allows architecture astronauts the ability to build elaborate footguns.
There's probably something wrong if a substantial project in C does NOT use gotos.
for (…) {
for (…) {
if (…) {
…
goto found;
}
}
}
found:
This is straightforward with goto and may even be vectorizable. I guess you could move the loop to a separate function or add additional flags to each loop, but neither of these seems like an improvement.In C++ we have something pretty similar already in the form of Folly ScopeGuard (SCOPE_EXIT {}).
__attribute__((cleanup(…))) is purely a scope-local mechanism, it has absolutely nothing to do with exceptions.
While it's true that it -fexceptions is disabled for C by default, some C libraries need to enable it anyway if they want to interact with C++ exceptions this way. For example C++ requires that qsort and bsearch propagate the exception thrown by the comparison callback normally, so libc implementations that are also used from C++ do enable it.
[1] https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attribute...
> some C libraries need to enable it anyway if they want to interact with C++ exceptions this way. For example C++ requires that qsort and bsearch propagate the exception thrown by the comparison callback normally
-fexceptions is not needed for this, C++ exceptions will just transparently bubble through C functions without its use.
I don't think I know of any project using -fexceptions… time to google…
It's a bit weird for the C++ standard to require behavior off C standard functions though, I gotta say… seems more sensible to use a C++ sort…
https://news.ycombinator.com/item?id=42532979
Interactions with stack unwinding are not considered by C, so I doubt this would be. Compilers would be free to make it work, however. This is just my guess.
After a few attempts at defer, I ended up using a cleanup macro that just takes a function and a value to pass to it: https://github.com/aws-greengrass/aws-greengrass-lite/blob/8...
Since the attribute or a function-like macro in the attribute position broke the c parsing in some tooling, I made the macro look like a statement.
https://thephd.dev/lambdas-nested-functions-block-expression...
(And also on -O1 and higher the entire call is optimized out and the nested function inlined instead.)
It's unfortunate a lot of the standards guys are horrified by anything that isn't C89. Because if the executable stack is an issue it's worth fixing.
Side note: 20 years ago people thought if they made the stack non executable that would totally fix stack smashing attacks and unfortunately it only slows down script kiddies.
Slowing them down is good. And: separating data and code helps simplify managing the caches.
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.
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.
> 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 hidden, and generated assembly will generally look like goto spaghetti even when your code doesn't.
What this implementation of defer does under the covers is not interesting unless you're trying to make it portable (e.g., to older MSVC versions that don't even support C99 let alone C23 or GCC local function extensions) or efficient (if this one isn't).
And when the machinery fails, you'll not only have the machinery to debug, but the syntactic sugar too.
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.
There are other options, but none of them is better, IMO. You can use nested functions:
char * p = malloc(10);
if(p!=0) {
doTheRealWork(p);
};
free(p);
In gcc, doTheRealWork can be a nested function or it you can force them to be inlined.You can also (more readable than that first alternative, IMO) wrap code in a single-iteration for or while loop and then use break to exit it:
char *p = null;
for(int i=0;i==0;i=1) {
p = malloc(10);
if(p==0) break;
…
}
free(p);
Both will get uglier if you need to free multiple resources, but will work.C++ destructors are great for this, but are not possible in C. Destructors require an object model that C does not have.
As for the n3434 proposal, given that the listed implementation experiences are all macro-based, wouldn't it be more easily adopted if proposed as a standard macro like <stdarg.h>?
returns_struct looks actually correct to me, ie it is expected (by me). Golang's defer works this way.
Do both examples follow standard? Or is it common misinterpretation by all compilers?
#define __DEFER__(V) __df_st const V = [&](void)->void #define defer __DEFER(__COUNTER__) #define __DEFER(N) __DEFER_(N) #define __DEFER_(N) __DEFER__(__DEFER_VARIABLE_ ## N)
#include <stdio.h>
struct S { int r; ~S(){} };
(i know hn will mangle this but i won't indent this on mobile...)
people really write cpp like this or is this a intentionally obscure example?
Cleaner version is like: https://github.com/llvm/llvm-project/issues/100869#issue-243...
Do languages need to grow in this way?
The overriding virtue of C is simplicity.
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.
No.
> Do languages need to grow in this way?
No.
> The overriding virtue of C is simplicity.
It's no longer simple, especially not with the C11 memory model (which got retrofitted from C++11, and is totally incomprehensible without reading multiple hundreds of pages of PhD dissertations); however, gratuitously complicating C shouldn't be done.
I agree that a feature like this could be useful, but then there are other useful features which should be added too. Where do you stop? I hope the C standards committee does not succumb to the feature bloat trend we see in many other languages (hand on heart: how many people fully understand/master all of C++/23's features?).
Need proper arrays instead of pointers to contiguous parts of memory which are interpreted as arrays? Proper strings (with unicode)? Etc. - use a different language suitable for the job.
We really need to let go of the notion that complex software must be written in a single language. Use C for the low level stuff where you do not need/want an assembler or where you want full control. For everything else there are more suitable tools.
Note that C++ destructors are also not ideal because they run solely based on scope. Unless RVO happens, returning a value from a function involves returning a new value (created via copy ctor or move ctor) and the dtor still runs on the value in the function scope. If the new value was created via copy ctor, that means it unnecessarily had to create a copy and then destroy the original instead of just using the original. If the new value was created via move ctor, that means the type has to be designed in such a way that a "moved-out" value is still valid to run the dtor on. It works much better in Rust where moving is not only the default but also does not leave "moved-out" husks behind, so your type does not need to encode a "moved-out" state or implement a copy ctor if it doesn't want to, and the dtor will run the fewest number of times it needs to.
Defer is a way better solution than having to cleanup in every single failure case.
you're only tempted to clean up [fully] in every single failure case if you don't know how to implement cascading gotos or the arrow pattern.
goto is widely considered an anti-pattern and so is the arrow pattern.
Yes, I am.
Agreed; they're terrible. Implicit sucks, explicit rules.
Really fix, not mitigations with their own gotchas.
That's because you've become complacent; you've gotten used to the false comfort of destructors. C++ destructors promise that you can ignore object destruction in business logic, and that's a false promise.
Assume you have a function that already has a correct traditional (cascading gotos, or arrow pattern) exit path / error path. Assume that you have the (awkward) defer-based implementation of the same function. Assume you need to insert a new construction step somewhere in the middle. In the defer-based implementation, you insert both the new action and its matching "defer" in the same spot.In the traditional implementation, you locate the existent construction steps between which you insert the new construction step; you locate the corresponding existent destruction steps (which are in reverse order), and insert the new destruction step between them. The thinking process is more or less the same, but the result differs: without defer, your resultant source code shows what will actually happen on the exit path, and in precisely what order, and you can read it.
I think defer is awful.
Without defer like mechanisms objects get leaked, mutexes held after return, etc...
In a perfect world everything could be perfectly explicit as infinite time and energy has gone into ensuring nothing is forgotten and everything / every code path is well exercised.
Even then, scoped based resource acquisition / releasing still feels more ergonomic to me.
https://github.com/google/honggfuzz/blob/c549b4c31815e170d3b...
#include <boost/scope/defer.hpp>
BOOST_SCOPE_DEFER [&] {
close(fd);
};Implementation here: https://github.com/boostorg/scope/blob/develop/include/boost...
We were using C++ and essentially instrumented code review processes, despite being a student group, to ensure nothing was ever called in a way where the destructor wouldn't be called - and broke out vision processing into a separate process so if other processes crashed we'd still be okay. In retrospect it was amazing training for a software engineering career.
But I always look at the words "simple" and "defer" and shudder when I see them next to each other!
Just like you'd have a "threat model" for cybersecurity, make sure you understand the consequences of a defer/destructor implementation not functioning properly.
This wasn't as bad as what you saw, as a restart would fix it (for a few minutes at least), but would disable us for the rest of a match if it ever happened at a competition.
At the end of the day, we worked around this by implementing a null pointer check. It was something along the lines if:
if (camera.toString().contains("(null)") return;
I assume there was still a race condition where the garbage collector would trigger whatever ffi destructor nulled out the relevent pointers; but we never ran into it.To your point, this looks to be an alternative to the 'goto end' pattern in C. In my experience that is the least bug prone pattern of destructors for C code.
¹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.
I don't know what GCC is doing, but functional programming languages usually 'compile away' the nesting of functions fairly early, see https://en.wikipedia.org/wiki/Lambda_lifting
Update: Oh, I see, it's because GCC doesn't want to change how function are passed around ('function pointers' in C speak), so they need to get creative.
Functional languages use something closer to what C people would call a 'fat pointer' to fix this.
Wouldn't you need an executable heap, though?
(That's not as outlandish, because eg a JIT would need that to. But most straightforward C programs don't need to create any executable memory contents at runtime.)
defer fclose(f);
Not a serious problem, just inelegant.I do think this matters in practice. If I have a `func whatever() error` IME it's a common to accidentally do something like `defer whatever()` without catching handling the error. To work around that you'd need to do something like the following.
var err error
defer func() {
err = whatever()
}
For me personally: ugh. I understand the "received wisdom" is to just structure your code differently. I don't think that's reasonable, because cleanup code is often complicated, often can fail, and is generally vastly less well-tested/exercised. YMMV, my experience is that, because of these things, the dispose-path bugs tend to be disproportionately bad. Error handling facilities that treat the dispose path as by-default not worth insuring are IMO setting users up to have a very bad time later.While we're fixing up the error handling, I really think it's not an accident that "`defer` in a loop does not terminate in the loop, but at the end of the function" is such a frequent misunderstanding. Yes the docs are clear, but all languages spend a lot of time training you on lexical scoping, and lexically-defined disposal lifetimes (e.g., C#'s `using` or Python's `with`) are a natural extension of that training. Those implementations to varying degrees have the same error handling problem, but with some effort I really think a better world is possible, and I don't think we have to expand this to full-on RAII to provide such semantics.
Like many other things in Go's approach to language design, still better than using plain old C, though.
The function signature is `void(MyType*)` if it is used like:
__attribute__((cleanup(MyTypeCleanupFunction)))
MyType myType;
If it's a pointer, it'll call the cleanup as a double ptr, and etc.
Note that the specified cleanup function can do anything you want to, though the intended purpose is to cleanup the variable when it goes out of scope.I know I hate one and love the other, but I still don't know which.
https://github.com/apple-oss-distributions/Libc/blob/Libc-16...
The explicit alternative for defer is an unreadable mess. And guess what? If you really need to see it, we should having tooling that desugars the defer to gotos. That's the best of both worlds!
- The "master" source code is high level "say what I mean"
- Low level view that is still much higher than dropping down into assembly still exists!
You really should be able to express the required cleanup semantics with "defer", and if you cannot, you can always just replace the original with the low level view and edit it instead --- good luck getting that past code review, however :).
I'm perfectly able to do that, I just don't want it, because the result is terrible. The syntax no longer shows what happens when and where.
And, if you look at n3434 <https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3434.htm>, you can see the following gem:
> the deferred block may itself contain other defer blocks, that are then executed in chain according to the same rules
This is the worst possible outcome. It means that a continuation passing style sub-language gets embedded in C.
{
defer { a(); defer { b(); }; c(); };
defer { d(); };
}
At the final closing brace shown, d() will be invoked first, then a(), then c(), then finally b(). Syntactically, the invocations appear in a-b-c-d order in the source code, but the actual execution is neither that nor the inverse d-c-b-a nor the single-level inverse order d-a-b-c.THAT is an unreadable mess. Good luck debugging that. Not dissimilar to chaining futures in modern async C++ (lambdas deeply nested in lambdas). Which is an abomination.
Your source code syntax is now completely detached from the execution order within a single thread.
Good luck getting that past code review.
Your reference to "no take only throw" makes no sense to me. The gist of that meme is "various characters making contradictory demands". Nobody is making demands here. There's no need for an "explicit alternative". Just don't defer at all, regardless of style. Write the error path / exit path explicitly, using gotos or the arrow pattern.
There is a style of grey-beard that never wants to reason in parts. He simple loads the whole program into his big brain and works from there: trees through the eyes, forest (maybe, certainly subjective) in the mind.
I too am big-brained, but I wholly reject this approach --- it makes for code that is sorely lacking in motivation --- all "what", no "why".
With "goto" I need to reconstruct what the arbitrary control flow means. It might be cleanup, it might be something else entirely! With "defer", we have the essence of RIAA without any bad OOP nonsense. The cleanup obligations themselves are literal in the code.
You don't need to know the order d, a, c, b run in 99% of the time. The reverse-order semantics indicate the LIFO semantics, which should be enough and tied to e.g. the deadlock avoidance or inter-resources-that-need-cleanup dependency management. And again, whenever you do want to see what the actual order is, the desugarer should always be a click away, perfectly your questions.