GOTOphobia considered harmful in C
blog.joren.ga
blog.joren.ga
It easy to misunderstand what he was even talking about because the paper is so short, assumes you know about this context, and has no concrete examples. People quite reasonably assume it's about ugly code they've encountered, but it's actually about ugly code of a completely different kind.
I'm not that old, but I was unlucky enough to have programmed in an unstructured language where GOTO was the only way to use faux-subroutines in my teens. Whatever you think as code that's difficult to follow: it's nothing compared to this.
Can't highlight this enough. The type of spaghetti code "goto considered harmful" was reacting to is basically impossible to create anymore, so anyone who didn't work on that type of code in the 80s or earlier probably hasn't seen it.
And thus, is applying the mantra "goto considered harmful" incorrectly. (Such as trying to avoid it in C for clean error handling, when there's no reason to avoid that.)
To try to replicate that experience, you'd have to write your entire application (including all libraries since libraries were often not a thing) as one single function in C. All of it, no matter how many tens of thousands of lines of code, all in one function. Then label every line. Then picture having GOTOs going every which way to any of the labels. For instance you'd preset the counter variable to some desired value and jump right into the middle of a loop elsewhere. Over time you'd surely accumulate special conditions within that loop to jump out. And so on. It's difficult to even imagine code like this today (or in the past 30 years).
Code with extreme branching, while dealing with state/boolean parameters that determine flow branching; error handling that can also create other branches of execution; all of that is really hard to keep in mind when reading such nightmare codebases...
Its still quite possible in assembly, where goto (JMP) is your only way to do control flow. But I doubt there's many people left who write and maintain large assembly programs. I imagine most programmers reach for C or something higher level as soon as the program becomes non-trivial.
I still use this cute online Intel 4004 simulator sometimes when I teach programming:
Its a fun challenge for novice or advanced programmers alike to write little programs in assembly for a CPU from 1971. The assembly language[1] is only 45 commands, and you only need a handful of them anyway. The CPU interpreter is simple enough you can literally see it think.
Yes, certainly very possible in assembly as it ever was. But as you note, few people are doing large scale assembly programs anymore. And I'd say that those who still do, are sufficiently experienced to avoid unstructured jump explosion in their code, hopefully.
Dijkstra would clearly disaprove of this use of goto. But he would blame the C language for making it necessary. Languages with structured cleanup (like the using clause in C#) does not need to use gotos for resource cleanup.
Dijkstras argument does not distinguish between short and long gotos or long or short functions. His argument applies to any use of goto.
If I look back it always comes back to naming and managing names of things. GOTO 100 is meaningless and one eventually runs out of meaningful names for GOTO labels. For me OOPs addressed the naming issue relatively effectively by using the data type as a namespace of sorts.
Structured programming wasn't about eliminating jumps, it was about enforcing discipline in their use. The simplest way to do that is to eliminate raw GOTO from the language, but it's also possible to just be careful and use it wisely.
Additionally, far more often than not, continue/break allow you to avoid another form of complexity, bugs, and low comprehensibility: deeply nested conditionals.
What makes break/continue (including labelled variants a la Java) useful is the fact that the restriction on where they can jump means that the control flow graph is guaranteed to be reducible. That is not the case with free-form goto.
The distinction matters because the whole premise of Dijkstra's argument is that if you replace the GOTO keyword with a bunch of more limited versions that cannot be used to produce spaghetti, code quality would go up. The only way for that to work is for the language to be semantically incapable of expressing GOTO.
As I said in my other reply, you seem to have the subtyping relationship wrong: GOTO is a subtype of BREAK (anywhere you find a BREAK you could replace it with GOTO), but BREAK is not a GOTO (you cannot do the reverse).
I dont fully agree with Dijkstras argument. For example I think early returns can often improve the readability of the code. But worth noting Dijkstra is not primarily concerned about readability but rather about how to analyze the execution of a program.
http://www.u.arizona.edu/~rubinson/copyright_violations/Go_T...
CONTINUE, BREAK, and GOTO all operate predictably because they are deterministic operations. Each continues program execution at the directed explicit (goto) or implicit (continue or break) offset. There is no non-deterministic or unpredictable behavior whatsoever.
If I sat you in front of a computer generating numbers using a pseudo random number generator and gave you as context the last number it generated, could you make any prediction about the next number it generates?
Now if it used a prng that was known and standardized to only compute one number could you predict anything about the next number now?
Seems like a reasonable trade for the occasional "goto again".
[Before anyone reads the above as advocating MISRA C ---- I think MISRA actually tells you not to do "goto fail;" which is advice I'm kind of dubious about. It also tells you to not do "good = good && side_effecty_thing();" (no shortcutting operators when there are side effects) so its style has you make a typical function absolutely littered with explicit initialization guards.]
To find an example, I did a search for “Commodore PET Basic programs”.
Here’s a book from 1979 that shows what spaghetti code looks like, in my opinion:
http://www.1000bit.it/support/manuali/commodore/32_BASIC_Pro...
The first program listing is on page 24 of the PDF. Try to follow the logic of the program. Why does line 400 go to 280? What paths can lead to line 400? Who knows! And this is high-quality BASIC by 1979 standards — it’s in a printed book after all.
There’s an auxiliary listing after the program itself explaining the routines and variables used, but many/most programs in those days wouldn’t have this level of rigorous documentation. Deciphering the program would probably have to start by drawing a flowchart of execution paths.
gwbasic had a function to relabel all the lines. And the labels skip by 10 numbers exactly for the purpose of inserting lines. Then when you had hit the limit you'd ask the computer to relabel them in steps of 10 again.
gw-basic was released c4 years after this book, and there was a huge change over that period:
1976 - Release of Apple I
1977 - Release of Apple II / Commodore PET
1979 - This book
1982 - Commedore 64
1983 - GW Basic
This book is pretty much closer to the Apple I than gw-basic. Perhaps I should have specifically said developing basic in 1979 though as referenced in that book though (and there isn't just one sort of basic - there are so many dialects).
Might be why I'm good at restructuring horrible to read spaghetti code.
I don’t think that’s true, certainly not for books of that time period. Because the whole field was changing rapidly, writers would often work under tight schedules, and customers would buy about anything because they only had magazines and books to learn from and review sites didn’t exist.
I also think that’s bad Basic for the time. Certainly, comment lines before subroutines would help.
You wouldn't want to waste characters on commenting code: machines of that era would have only a few KB of RAM, as low as 1K. For the same reason you don't want to waste characters on long, meaningful variable names or on well spaced code. Multiple statements per line isn't to save print space, it's to save RAM.
Meanwhile, the program's pretty well structured for such a short bit of BASIC: subroutines start at multiples of 100, for example, and each subroutine starts and ends clearly, no shenanigans like jumping from the middle of one sub to another, no multiple exit points for subs, all as linear as it can be. The use of IF is limited to skipping forward a short way to conditionally execute a line or two only. GOTO only exists in those IF statements.
I'd have been happy to have written code like this, back then.
I am pretty sure that my uncle ran this exact program on his computer and printed out biorhythms on listing paper, in the mid 80s.
There is a simple thing. On a lot of machines only spaghettified programs would even fit in the memory available. Academic CS researchers with their unlimited accounts on the institutions mainframe didn't have that worry.
It’s like an iceberg of bad code: the underwater part nobody saw was astonishingly terrible by modern standards. That code might be running a business, but its author would never get exposed to professional programming. Today Excel often serves a similar purpose. (Excel isn’t spaghetti though since the execution model is completely different.)
That code looks normal to me. I have a ton of BASIC books and magazines. You're talking about a time period before full screen text editors were a thing. It's almost impossible to explain to anyone that didn't have to work with TI-99 BASIC, C64 BASIC, GW-BASIC/BASICA, etc. what it was like. Once you got to QBASIC/QuickBASIC it was done. Life was easy. A few years before that, and you're printing out pages on a dot matrix printer and going line-by-line to debug. You'll notice a distinct lack of white space between lines and that comments start with "REM" and a line number. You didn't even get labels for lines. The code looks like that because those were technology limits on really rudimentary devices. You were editing code inside the BASIC interpreter, often using some command like "LIST <line#>". It was awful.
We take so much for granted today. Dual monitors. Color. More than 80x25 character display. Multiple screens and multitasking. Just getting to Linux in '95 and having F1/F2/F3/etc. switching terminals was a huge deal.
I remember chasing bytes with short variable names, abbreviated print statements, reducing spaces as much as possible etc.
I had like 28k to play with and I was 14 years old!
10 for i = . to 6.28 step 0.1:next
and it would be slightly faster and smaller than 10 for i = 0 to 6.28 step 0.1:next
Made for some ugly inner loops, but you gotta do what you gotta do. For that matter, we certainly would have removed some of those extra spaces as well. Bytes mattered and whitespace slowed you down.Without looking at the post-program material, this isn't exactly a difficult question to answer.
Line 400 is preceded by some print statements:
370 PRINT "PRESS 'E' TO END, SPACE TO CONTINUE"
380 GET R$:IF R$="" THEN [goto] 380
390 IF R$="E" THEN [goto] 120
400 L=0:GOTO 280
So we have a prompt that says "press E to end, space to continue", and then branches one of three ways: if you provide no input, the prompt is shown again; if you provide an E, the entire program restarts from scratch, and if you do anything other than that, the count of lines drawn on screen is reset to 0 and the next 18 lines of the chart are drawn.We can assume that line 400 will be hit whenever a piece of chart is drawn to the screen.
The program's structure here is a nested loop: there is a loop between lines 280 and 400 (displaying the chart indefinitely, 18 lines at a time) containing another loop between lines 300 and 360 (displaying 18 lines of a chart, one line at a time).
Why is this supposed to be an example of spaghetti code?
400 L=0:GOTO 290
This would be almost impossible to debug. /* 280 */
do_something();
for (;;) {
/* 290 */
/* display chart in blocks of 18 lines */
/* 400 */
L = 0;
}
when the correct code is this: for (;;) {
/* 280 */
do_something();
/* 290 */
/* display chart in blocks of 18 lines */
/* 400 */
L = 0;
}
The bug is that the call to do_something() precedes the outer loop when it should be inside the loop.Is that easier to debug in C than it is in the BASIC program? What's the difference?
Loops would look like:
int i = 0;
loop_start:
print i;
i = i + 1;
if (i < 10) goto loop_start;
Fizzbuzz would look something like: int i = 0;
loop_start:
if (i % 5 != 0) goto not_fizzbuzz;
print "fizzbuzz";
goto done;
not_fizzbuzz:
if (i % 3 != 0) goto not_fizz;
print "fizz";
goto done;
not_fizz:
if (i % 5 != 0) goto not_buzz;
print "buzz";
goto done;
not_buzz:
print i;
done:
i += 1;
if (i < 100) goto loop_start;
Often, this wasn't just constrained to a single function; the whole program would be constructed like this, with gotos which jump back and forth across pages and pages of code. Languages wouldn't even have a call stack with subroutines (which is why "procedural" languages -- languages with procedures -- were important enough to be given a special name).At least that's my understanding of it. I haven't lived through this, and my only experience with this kind of stuff is writing assembly, where we always make use of a call stack, so even that is in practice a procedural language. If I have gotten anything wrong, please correct me.
https://github.com/Keith-S-Thompson/fizzbuzz-c/blob/master/f...
#include <stdio.h>
#include <setjmp.h>
int main(void) {
jmp_buf jb[7];
volatile int j = 0;
setjmp(jb[0]);
volatile int i = 1;
if (j == 0) setjmp(jb[1]);
if (j == 1 && i > 100) longjmp(jb[6], 0);
if (j == 1 && i % 15 == 0) longjmp(jb[4], 0);
if (j == 1 && i % 3 == 0) longjmp(jb[2], 0);
if (j == 1 && i % 5 == 0) longjmp(jb[3], 0);
if (j == 1) printf("%d\n", i);
if (j == 1) longjmp(jb[5], 0);
if (j == 0) setjmp(jb[2]);
if (j == 1) puts("Fizz");
if (j == 1) longjmp(jb[5], 0);
if (j == 0) setjmp(jb[3]);
if (j == 1) puts("Buzz");
if (j == 1) longjmp(jb[5], 0);
if (j == 0) setjmp(jb[4]);
if (j == 1) puts("FizzBuzz");
if (j == 0) setjmp(jb[5]);
i ++;
if (j == 1) longjmp(jb[1], 0);
if (j == 0) setjmp(jb[6]);
j ++;
if (j < 2) longjmp(jb[0], 0);
}OTOH early FORTRAN had procedures with arguments and results, but no recursion.
Structural or not was also not necessarily all-in. FORTRAN and BASIC both had for-loops before they had structured conditionals.
loop_test: IF (NOT loop_condition) GOTO after_loop
loop_body
GOTO loop_test
after_loop: etc...Exactly! Recently I had the "pleasure" to work with some FORTRAN IV code from the early 60s, so I know what you mean. No functions/subroutines, only GOTOs. Even loops were done with labels. There is also a weird feature called "arithmetic IF statements" (https://en.wikipedia.org/wiki/Arithmetic_IF). Luckily the code was pretty short (about 500 lines including comments).
There’s no good way to do a do…while loop in Fortran, other than a goto.
INTEGER A(4,4), C, R
...
DO 10 WHILE ( C .NE. R )
A(C,R) = A(C,R) + 1
C = C+1
10 CONTINUE
Note that Fortran has evolved significantly over time. This is how you would write the same in Fortran 77: INTEGER A(4,4), C, R
...
C = 4
R = 1
DO WHILE ( C .GT. R )
A(C,R) = 1
C = C - 1
END DO> There seems to be a unfortunate inconsistency in the naming of this thing.
Well, Fortran is older than C, so you cannot really blame them :-)
Forty years later, I'll still use a C goto if the situation warrants (e.g. as a getout from deep but simple if). Maybe because having long been an assembler programmer as well, goto's are part of the landscape (if/else is effectively a conditional and unconditional branch/jump).
I would say js/python callback-based frameworks of today like Twisted (also c++/rust futures) is exactly that
GOSUB was so much worse than GOTO in that it had a stack for the current line but no stack for variables so you could not write recursive functions. I think Fibonacci as a recursive function is malpractice but boy was it a hassle to write Quicksort in BASIC although I had no trouble coding up an FFT (1950s FORTRAN style) from first principles in BASIC on TRS-80 Model 100 on a bus ride across Vermont.
Funny though I did come to a conclusion that for the Arduino programs I wrote I didn’t need a stack at all.
Was that a Casio calculator? Because it was like that for me, only GOTOs existed. Learning about C and seeing these things called loops was a revelation because I had reinvented them with GOTOs already in my programming.
Malicious spaghetti involves transformations such as
for (x = 0; x < 10; x++) {
for (y = 0; y < 20; y++) {
printf("%d %d\n", x, y);
}
}
|
| DRY
v
x = 0;
loop_x_head:
condition_val = x;
condition_stop = 10;
condition_var = 'x';
goto check_condition;
loop_x_body:
y = 0;
loop_y_head:
condition_val = y;
condition_stop = 20;
condition_var = 'y';
goto check_condition;
loop_y_body:
printf("%d %d\n", x, y);
increment_val = y;
condition_stop = 20;
increment_var = 'y';
goto increment;
loop_y_end:
increment_val = x;
condition_stop = 10;
increment_var = 'x';
goto increment;
loop_x_end:
halt;
increment:
increment_val++;
if (increment_var == 'x')
x = increment_val;
if (increment_var == 'y')
y = increment_val;
condition_val = increment_val;
condition_var = increment_var;
check_condition:
if (condition_val < condition_stop)
goto pass_condition:
if (condition_var == 'x')
goto loop_x_end;
if (condition_var == 'y')
goto loop_y_end;
pass_condition:
if (condition_var == 'x')
goto loop_x_body;
if (condition_var == 'y')
goto loop_y_body;
[1] http://wigfield.org/RND_HAR.BASI grew up with MSX-BASIC: https://github.com/plattysoft/Modern-MSX-BASIC-Game-Dev/blob... – GOSUB jumps to a specific line number (RETURN returns from where it jumped). Even a fairly simple and clean example like this can be rather difficult to follow.
Just like 8 bit BASIC spaghetti code, only refined.
However, while I sympathise with a C programmer who feels goto is necessary in their language, I think that speaks to a problem in the language rather than the programmer. If you have better structural support in your language you can express the things the author (of the link, not Dijkstra) wants to express without needing this unstructured go-to.
For example Rust's break 'label value; allows us to mark any compound expression with the 'label, and then say from anywhere inside that expression but nowhere else that we've decided the value of the expression overall and here's what it is.
This doesn't feel that different from what is being done here with goto, except for two crucial things as a result of being structured:
1. Rust will type check this, if this region of the program picks a Dog, our break needs to provide a Dog, it can't just shrug and expect the program to continue without one. This means maintenance programmers don't need non-local reasoning, this region of the program does, in fact, always pick a Dog, albeit the break 'label value is something to look closely at if you're reading that region itself.
2. We cannot do this, even by mistake (e.g. as a result of copy-paste) across scopes. If you try to break 'label result from the cat care loop into the dog loop earlier in the same function, that just doesn't compile, whereas the C goto has no problem attempting that (a good C compiler should notice if you try to do something really egregious, but good luck).
Dijkstra is clearly arguing for a “single entry single exit” style. But modern consensus seem to uphold single entry but accept multiple exits from a block. Break, continue, early returns, exceptions - all are example of multiple exit. These are more constrained than gotos but nevertheless Dijkstras argument applies to them also.
I personally belive early returns can greatly improve readability (when not nested too deep) an that exceptions are typically cleaner than the alternative. But I acknowlede Dijkstra would disagree.
Break, continue, and early returns always return to the end of the block, unlike goto in the '70s. Exceptions are more complicated, and the cause of a lot of inunderstandable programs.
[1] https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
Sure, but that has nothing to do with whether the GOTO statement should be used. The semantics of GOTO are arguably quite clear; they amount to setting a different continuation for the running program. (Semantically, this implies that the precondition of the GOTO statement is made a possible precondition for the label that the statement jumps to; and execution simply does not proceed to the next statement.)
There are even "relooper" algorithms to reconstruct a structured program from patterns in the idiomatic use of GOTO statements: they are used in compiling to structured object languages such as WASM code. Using GOTOs in an idiomatically sensible way (avoiding spaghetti code) may be less readable than writing actual structured blocks, but only slightly so.
One I see all the time from beginners is creating a finite state machine using one method per state and jumping between states by calling the next state’s method from within the current state’s method. Essentially just emulating goto with the added disadvantage that you’re pushing each new state onto the stack until it overflows, so refactoring it using goto would constitute an improvement.
The fact that beginners reinvent this pattern over and over demonstrates it’s easier for people unfamiliar with programming to reason about a program that uses goto, which would explain why it was so ubiquitous in the early days of computing and given its shallower learning curve its usefulness as a teaching aid as a first step before structured programming is being overlooked.
But you can do the same with an event loop that cleanly avoids goto.
[1] https://clang.llvm.org/docs/AttributeReference.html#musttail
[2] https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
this is genuine question, because I do believe that people do not use enough state machines to model their systems.
My problems have always been in how memory corruption friendly C happens to be, nothing to do with how to use structured programming practices for resource cleanup.
The structuredness of such inferred jumps usually is both sufficient and convenient, but sometimes being extremely structured leads to boilerplate and inefficiencies. That shows already with early returns from nested scopes, which aren't widely frowned upon, while they are very similar to common usage of goto. I would say I haven't encountered a goto that isn't basically an early return from some nested scope to after some parent or grand-parent scope.
In that sense, a goto can save from having to extract a nested scope as a stand-alone function just to write it as an early return. I would say that many gotos in the wild could be rewritten as early returns after making a standalone function, but maybe sometimes this is too much of a hassle, or, subjectively, puts a toll on readability.
As for structured handling, instead of gotos all over the place, do inverted conditions for early returns similar to what Swift has done with guard statement, embrace functions for resource cleanup, if the cost of a jump brings cold sweat, have them inline and called alongside a return, most compilers will replace calls with jmp opcodes.
What?
> guard
How does it help to have an inverted if statement? Not sure what is the point, can do without.
Goto isn't necessarily for resource cleanup. The common usage is as an early break from the currently executed block, to what comes after the block. Which is often cleanup code, but not necessarily.
I'm absolutely coding in C again — writing some old-school games using SDL. After working for decades with various retain/release, garbage-collected, magic-memory™ languages, going back to C feels like programming again. I like it.
Somewhere between 95% and 100% of people who think they're "steeped in C and its quirks, have good coding patterns that allow for it" write code that invokes undefined behaviour.
No, not "the language", but "the standard".
Languages like Python or Rust that don't have standards are 100 percent undefined behavior.
If you're really worried about this, then just code against x86_64 clang and not ISO C++ and forget about this non-problem forever.
The only sin of C++ is the same that plagues evolutionary languages like Typescript, Kotlin and so forth.
No matter how many tools they provide to write better code than the language they have grown from, their compatibility with them is like hearding cats while trying to have everyone adopt best practices.
Otherwise in regards to Arduino, I would suggest a couple of nice BASIC and Pascal compilers like those sold by Mikroe.
shhh. Let's not disturb those that believe in the GC fairy that sprinkles their code with safety magic late at night while they soundly sleep. Firmware doesn't exist. I can't hear you. Na na na na na na na
https://www.withsecure.com/us-en/solutions/innovative-securi...
A good indication of embedded is having to manage shared global state.
This is very unlike a browser where you can hide all that behind OS calls.
And costs the same as an ARM with dozens of megabytes of each.
However, I always find myself missing it when writing nested loops. Labeled break and continue[1] ought to be considered standard structure programming primitives. These are restricted gotos and allowing them to break or continue a parent loop doesn't unrestrict them much. But it does significantly improve the expressive power of your looping constructs.
[0] C++, Rust, Go, or anything else with automatic memory management and destructors
[1] I've also heard of numbered break/continue. Personally I think this isn't good enough: what if I need to move loops around? That will change the meaning of a `break 2`. With a `break OUTER` the compiler will yell at me if I remove the outer loop without changing all the code that breaks out of it to target a different one.
RAII simplifies some of the resource cleanup, but at a cost: if the resource cleanup fails, there’s essentially no way to convey this.
So yes, you can write a destructor that tries to clean up your resources regardless of how the code exits its current scope. But if that cleanup encounters a problem, the destructor can, at best, try to convey this indirectly, either by (commonly) logging or (rarely) setting a variable to a value. It certainly cannot throw another exception, or even directly manipulate the return value of the function.
This is fine for some purposes, and completely unacceptable for others. But it’s not equivalent to the explicit cleanup and error handling in C.
So some of us occasionally find ourselves in an RAII language and using goto.
RAII at least provides a decent default. After all, most resource cleanup cannot fail, or you cannot do much other than print an error. Now, if you need to catch errors during a particular cleanup, you can still do it manually, but RAII lets you focus on these few cases.
Goto + RAII isn’t generally compatible unless you structure things very carefully
Also why it’s widely regarded as _wrong_ to ever throw in a destructor.
In most cases, an second exception raised while another exception is already being thrown is merely a side-effect of the first exception, and can probably safely be ignored. If the idea of throwing away a secondary exception makes you uncomfortable, then another possible solution might have been to allow secondary exceptions to be "attached" to the primary exception, like `std::exception::secondary()` could return an array of secondary exceptions that were caught. Obviously there's some API design thought needed here but it's not an unsolvable problem.
If we could just change C++ to work this way, then throwing destructors would be no problem, it seems? So this seems like a C++-specific problem, not fundamental to RAII.
That said, there is another camp which argues that it fundamentally doesn't make sense for teardown of resources to raise errors. I don't think you're in this camp, since you were arguing the opposite up-thread. I'm not in that camp either.
Java has that for its pseudo-RAII "try-with-resources" statement: when an exception happens during cleanup of a try-with-resources statement, and the cleanup was because of an exception (instead of normally leaving the block), the inner exception is added to a "suppressed" list in the outer exception. Java exceptions have, since Java 7 (which added try-with-resources), both a "cause" field (for the exception which caused that exception, this exists since Java 4) and a "suppressed" field (which records the exceptions suppressed while cleaning up that exception).
And C# does not actually allow you to throw null. It does allow you to write "throw x" where x may be null, but that will just cause an immediate NullReferenceException at runtime.
I am personally on team crash - I would rather my program exited and restarts in a known state then being in some weird and hard to replicate configuration.
Crashing makes sense in these scenarios if the application is only doing one thing. But I am usually working on multi-user servers. I would rather fail out the current request, but allow concurrent requests from other clients to continue.
Yes, I understand the argument: "But if something unexpected happened, your application could be left in a bad state that causes other requests to fail too. It's better to crash and come back clean."
This is not my experience in practice. In my experience, bad states that actually poison the application for other requests are extraordinarily rare. The vast, vast majority of exceptions only affect the current request and failing out that request is all that is necessary. Taking down the whole process is not remotely worth it.
Moreover, crashing on assertions has the unintended consequence of making programmers afraid to write assertions. In a past life, when I worked on C++ servers at Google, assertion failures would crash the process. In this argument, I saw some people argue that you should not use assertions in your code at all! Some argued for writing checks that would log an error and then return some sort of reasonable default that would allow the program to continue. In my opinion, this is an awful place to end up. Liberal use of asserts makes code better by catching problems, making the developer aware of them, and avoiding producing garbage output when something goes wrong.
Not unlikely. Sometimes your unwind involves cleaning up things that throw for the same reason as the original failure - e.g. failure to communicate with some piece of hardware. But you still try going through that unwind, right? Eventually you leave the context of accessing your hardware device entirely and are back to just working with system memory, the standard streams and some files, which would probably work fine.
I have recently experienced this writing wrappers for the CUDA API for GPU programming.
[0] https://en.cppreference.com/w/cpp/error/nested_exception
Anyway, the key missing thing is not so much the exception representation, but the ability to have custom handling of what to do when an exception is thrown during unwind. Today, it goes straight to std::terminate(). You can customize the terminate handler, but it is required to end the process.
Exceptions.
I use goto rarely. But there are times when anything else would be inelegant, and in those instances I'll use it without hesitation.
Go's automatic memory management (GC) doesn't come in to play here, but the defer statement does. It doesn't make Go a RAII language, but it makes Go a language with a nicer "run this code at the end of the stack frame" feature than goto.
You don't need them for teardown, but they still make sense for retries -- cases where a procedure needs to start over after hitting certain branches, e.g. a transaction conflict. I think `goto retry` is a lot more readable than wrapping the procedure in `do { ... } while (false)` and using `continue` to retry.
`goto` works very nicely with RAII here in that it'll invoke the destructors of any local variables that weren't declared yet at the point being jumped back to.
I find that normally if I need nested break then it suffices to refactor the target loop into a function and use return instead.
I don't think I normally miss multilevel continue, but the same strategy would work for that too. You'd just pull out the target loop body rather than the whole loop.
If that doesn't work because you need to select too many different break/continue levels (more than two) then maybe it's time to review the complexity of the function anyway.
that's a "good solution" in the sense of "lambda is the ultimate goto", but if you're writing a loop over the rows and columns (or more dimensions) of something, pulling out and segregating the control structure for the innermost (and potentially other) layers of the hierarchy can make a simple depth first exploration look obscure. If the total amount of code is fitting in a screenful, I'd rather put multi-level break or continue labels and then goto them sparingly.
That might make the code more complicated, overall, but that's the trade off of structured programming - occasionally there's more complexity but it's so exceptionally rare that it's still worth it overall. (Then again ... perhaps it would make the code arguably simpler anyway.)
tbh I see FAR more range-var-misuse with Go loops than defers. I've seen over 100 range-var problems, and seen lints catch many more (several thousand), but I've only seen a loop defer issue once. When a defer is needed, it seems like people both remember the issue better (`defer` is a new construct for many, `for` is not and habits from other languages can mislead them), and the code is complex enough to justify a helper func, where a defer is trivially correct.
... why isn't this in C already?
int* foo(int bar) {
int* return_value = NULL;
if (!do_something(bar))
goto error_1;
if (!init_stuff(bar))
goto error_2;
if (!prepare_stuff(bar))
goto error_3;
return_value = do_the_thing(bar);
error_3: cleanup_3();
error_2: cleanup_2();
error_1: cleanup_1();
return return_value;
}
The D version: int* foo(int bar) {
scope(exit) cleanup1();
if (!do_something(bar))
return null;
scope(exit) cleanup2();
if (!init_stuff(bar))
return null;
scope(exit) cleanup3();
return do_the_thing(bar);
}
https://dlang.org/articles/exception-safe.html let mut tx = Transaction::new();
dofoo(&mut tx)?;
dobar(&mut tx)?;
tx.commit();
There is some overhead to boxing the rollback functions for dofoo/dobar into the transaction object, but it's far less error prone (or maybe you can avoid the boxing by encoding all the rollback operations at the type level: less ergonomic but not by much).In C# the idiomatic way would be to have each of these 3 things be defined in a class using IDisposable, which is similar to D's scope() -- the declaring class gets a cleanup method when the variable goes out of scope, no matter how that happens.
I assume there's some interaction between these classes, but IMHO that should be explicitly defined and so the code would look something like:
public void foo(int bar) {
using var something = new Something(bar)
if (something.do()) {
using var stuff = new Stuff(bar);
if (stuff.init()) {
using var stuff2 = new Stuff2(bar); // two "stuff"s looks dumb but this is example code
if (stuff2.prepare()) {
return do_the_thing(something, stuff, stuff2, bar);
}
}
}
return null;
}
There's actually several ways to structure this code which would result in something that looks better than the above, but being example code and not knowing how `something` and `stuff` interact, it's hard to write this nicely. I'd probably aim for something much more concise like: public void foo(int bar) {
using var something = new Something(bar);
using var stuff = new Stuff(something);
using var stuff2 = new Stuff2(stuff);
return stuff2.prepare() ? do_the_thing(stuff2) : null;
}
In the above, I assume stuff2.prepare() calls everything it needs to on the dependent objects, but how I'd structure this for real entirely depends on what they're actually doing.But rest assured, your C# code is full of global state hidden on its runtime and is subject to the same kinds of errors people are discussing here.
int* foo(int bar) {
int* return_value = NULL;
__cleanup__((cleanup_1)) int c1 = 0;
if (!do_something(bar))
return NULL;
__cleanup__((cleanup_2)) int c2 = 0;
if (!init_stuff(bar))
return NULL;
__cleanup__((cleanup_3)) int c3 = 0;
if (!prepare_stuff(bar))
return NULL;
return_value = do_the_thing(bar);
return return_value;
} push RBP
mov RBP,RSP
sub RSP,020h
mov -020h[RBP],RBX
mov -018h[RBP],R12
mov -010h[RBP],R13
mov -8[RBP],EDI
mov EBX,-8[RBP]
mov EDI,EBX
call _D5test312do_somethingFiZi@PC32
test EAX,EAX
jne L2F
xor R13D,R13D
mov EBX,1
jmp short L79
L2F: mov EDI,EBX
call _D5test310init_stuffFiZi@PC32
test EAX,EAX
jne L4A
xor R13D,R13D
mov R12D,4
mov EBX,4
jmp short L68
L4A: mov -8[RBP],EBX
mov EDI,-8[RBP]
call _D5test312do_the_thingFiZPi@PC32
mov R13,RAX
mov R12D,7
mov EBX,7
call _D5test38cleanup3FZv@PC32
L68: call _D5test38cleanup2FZv@PC32
cmp R12D,4
je L79
cmp R12D,7
jne L8B
L79: call _D5test38cleanup1FZv@PC32
cmp EBX,1
je L8B
cmp EBX,4
je L8B
cmp EBX,7
L8B: mov RAX,R13
mov RBX,-020h[RBP]
mov R12,-018h[RBP]
mov R13,-010h[RBP]
mov RSP,RBP
pop RBP
retThe correct pattern here is RAII, which requires no explicit cleanup code so there's no forgetting to use it.
I've been reading a lot about retro gaming lately, so I'm just thinking in the context of bedroom coders of the 80's that learned to program in BASIC, then moved on to assembly to get more performance, and then later moved on to C and C++ as projects became more complicated. They all seem to have turned out okay.
I suppose what I'm getting at is you can write bad code in any language.
Nowadays there's more consideration given to structured programming and that's a good thing but that doesn't necessarily mean that GOTO should never be used—and if it is then it doesn't mean the whole structure of one's program ought to be called into question.
No doubt GOTO can be dangerous and can lead one into bad habits but in certain instances it can simplify code and make it less prone to introduced bugs. Modern coding practice teaches us to recognize and avoid spaghetti code so with those constraints in a programmer's mind he/she should be able to use GOTO effectively and with safety.
The key issue is to know when it's appropriate to use it and when not to.
The same is true of while loops. Aside from a few cases where they are required, they are always better rewritten with a less primitive operator (for, etc). The arguments that programmers today make in defense of while are quite similar to the arguments programmers used to make in defense of goto.
E.g. even an odd problem. Let's use GNU C with local functions:
#include <stdbool.h>
bool even(int x)
{
auto bool odd(int x);
bool even(int x)
{
if (x == 0)
return true;
else
return odd(x - 1);
}
bool odd(int x)
{
if (x == 0)
return false;
else
return even(x - 1);
}
return even(x);
}
Using goto: achieved by a mechanical transformation involving just some local edits: bool even(int x)
{
goto start;
even:
{
if (x == 0)
return true;
else
{
x = x - 1;
goto odd;
}
}
odd:
{
if (x == 0)
return false;
else
{
x = x - 1;
goto even;
}
}
start: goto even;
}
Every tail-called local function just becomes a block headed by a goto label. The tail call is replaced by assigning a new value to every argument variable and performing a goto. Someone who is briefed on the approach here can easily see the original tail recursion and maintain the code in such a way that the tail recursion could always be recovered from it.There was a discussion several years ago in comp.lang.c where a problem was proposed: using whatever approach you see fit, write a C program which strips comments from C code, but preserves everything, including preprocesor directives. Something like that. The person who proposed the problem refrained from posting his solution for several days. He used tail recursion for the entire state machine of the thing (even avoiding if statements; all the cases in the tail functions were handled by the ternary ?: operator).
Others used structured programming: nested loops and such. My solution used goto.
I argued that the goto solution had all the good properties of the superior tail calling solution.
I then supported my argument by writing a text filter which converted that person's tail call program into one with a big function containing goto blocks (compiling and producing the same result and all). A reverse filter would be possible also.
I believe that we can take any mess of a goto graph, divide it into the labeled nodes, round up the variable and everything being done to them and express it as tail recursion. Ironically, the one thing that will make it a bit harder is structured control flow constructs like while, for, switch and what not, where we may have to rewrite those to explicit goto first! E.g. if we look at a while loop, it's like a tail call, but one which is invisible. The end of the while loop body invisibly tail calls to the start, which is bad for understanding.
The thing that will detract from the ability to understand the tail call graph is excessive parameters. In the worst case, every tail function will have to take all of the state variablews as parameters, and pass them all to the next tail function (except for altering some of them). There is a pass that can be done over that to reduce some of these. Like if some tail function foo(a, b, c, d, e, f) doesn't do anything wiht c d e f other than pass it to children and none of those children do anything with those variables (transitively), we can cull those parameters from foo and all the children. This is the hard thing to understand in goto graphs: which of the numerous state variables are relevant to where the goto is going?
Some state vars can be replicated and localized. E.g. in our even() example, we can do this:
bool even(int x)
{
// params of even:
int x0;
// params of odd
int x1;
goto start;
even:
{
if (x0 == 0)
return true;
else
{
x1 = x0 - 1;
goto odd;
}
}
odd:
{
if (x1 == 0)
return false;
else
{
x0 = x1 - 1;
goto even;
}
}
start:
{
x0 = x;
goto even;
}
}
Now we no longer have the same variable on both sides of an assignment. Each block works with its private parameter variable. The other block only every assigns to that variable when simulating parameter passing: e.g. the odd block assigns to even's x0 just before goto even.We can start with a goto graph and make incremental improvements like this and recover a tail call graph. We can then try to understand what the tail functions mean in terms of recursion and document that.
tail recursion is a complex and subtle expression of control flow that requires substantial background knowledge to be able to even understand, much less reason about based on code on a page
for loops are immediately intuitive to anyone, even without any programming training
no idea how you can come to this conclusion. just ain't so
That is simply nonsense; it's just function application, ideally without having to think about state (or as little state as possible).
> for loops are immediately intuitive to anyone, for loops are immediately intuitive to anyone
That's just hand-waving without some sort of psychological data. Even if it were true, it would not be relevant because you can't just hand over software maintenance to just "anyone" pulled off the street who finds some language feature intuitive. (In my anecdotal experience, on the contrary, non-programmers have a very poor intuition for the idea of changing variables by assignment and what that means when a backwards jump takes place.)
A solution expressed recursively is demonstrably, quantifiably easier to reason about informally or prove correct than loops and state variables. Of course, not to "anyone" with no programming background just pulled off the street, but to people who have the understanding and skills. There is simply less proof material. An entire body of proof techniques that are required with imperative loops are absent. You just use straightforward inductive reasoning instead of pre and post conditions over stateful variables, and loop invariants and whatnot.
Anyway the idea that for loops are somehow easier than recursion is definitely not from mainstream CS; just some fringe view fr
I have never in my life heard a programmer say they think recursion is easier than loops.
Recursive solutions being easier to verify is quantifiable. This is not some popularity poll.
Some people are not well-versed in some techniques. Recursion is not always well supported in programming languages. In standard C if we want to use recursion, we will have to write multiple standalone functions that have their own scopes, whereas if we put together several loops, we can have those all in the same scope, with convenient access to common local variables. That could make a decisive difference. Not all languages have tail calls; what looks like tail recursion can "blow the stack".
When I say that it's "easier" I don't mean that anyone of any skill level and background can more easily design and implement a recursive solution for any problem, and in any language. Rather, that when the recursive solution is discovered, it is easier to convince oneself that it is correct: that it's handling all the cases and terminates, with the correct value.
Ease of doing something is a quantifiable description?
programs are recipes, not proofs. "for 10 times, do this" is in almost all cases trivially easier to understand and maintain than a recursive alternative. this isn't controversial in any way
People mocking the use of goto is a bozo bit switch for me. It can switch back, but offering pithy out of context absolutisms and trying to pass it off as wisdom is a hard point to recover from.
It's almost always in one's interest to play dumb and not be sure of anything. That's what I see the smart people do.
Then I saw it implemented recursively in lisp…it was the simplest most obvious choice to make, and it was hard to imagine anything else.
Now I think that the BASIC implementation must have been a translation from the lisp or something equivalent and it would have been very unlikely that any native BASIC programmer would have come up with it on their own. Of course it used GOTOs.
With the proper abstraction level any complex algorithm becomes trivial.
if (oksofar) {
oksofar = something_done = do_something(bar);
}
if (oksofar) {
oksofar = stuff_inited = init_stuff(bar);
}
and so forth.Also worth reading is "GOTO Considered Harmful Considered Harmful": https://news.ycombinator.com/item?id=11056434
And here Microsoft provides us with lovely example of such ridiculous nesting.
That's a very memorable example, but ultimately the true cause of that monstrosity is a clearly stupid API design; this is the API for a file picker, the recommended replacement for an existing one that they wanted to deprecate. In the existing one, you fill in a structure and call a single function with a pointer to it. In its replacement, you need to call a dozen methods on an object, and check for "possible" errors on each call, even if probably 99% of them only do things like assign to a field in a now-opaque structure and can never produce an error. Then the example code must've been edited by someone with severe gotophobia. (Not all MS code is like that --- they have plenty of other example code that uses goto, e.g.: https://github.com/microsoft/Windows-driver-samples/blob/mai... ) The existing API was even extensible, since it used a structure with a size field that could differentiate between different versions and extensions, but they didn't.
So it might look like:
do {
if (false == call_func1()) {
cleanup_any_state();
break;
}
if (false == call_func2()) {
cleanup_any_state();
break;
}
} while (0);
At any point you can branch to the common exit-statement, and keep on testing for failure as you go through the algorithm without indenting forever.Generally there's no too much state to clean up in the code I've been using this in, but obviously later 'break' conditions would have to clean up the state for earlier ones too. That's easy to abstract into functions for the cleanup though.
Sorry, a little off topic I know.
All of that has to be ok before you finally get to the bit that does the work, and since I'm using ObjC with its ARC feature, I don't need to deallocate anything, they'll be deallocated as they go out of scope. I tend to release critical RAII stuff in my -dealloc method anyway if there's anything there to be done.
So it's really a whole long list of
id result = nil;
do {
if (setup-X-fails)
break
if (setup-Y-fails)
break
...
result = call_method(X,Y,Z,A,...F)
} while (0);
... which works out pretty well, and is very readable. The setup-xxx stuff can be several pages of code for each method - and useful in their own right, so integrating it into the loop doesn't seem preferable.I know for C it’s unthinkable to standardize such a luxury like defer or destructors, so we’re going to relive arguments from 1968 for as long as C is used.
There was a proposal for defer in C23 but it didn't make the cut [1]. There is also the __cleanup__ attribute if you're using GCC.
[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2895.htm
https://www.opensourceforu.com/2012/02/function-pointers-and...
Then I looked up 'callbacks considered harmful'. Hmmm....
It's probably easier for an IDE to track down a function if you want to see how the code works, however, and the function might have some useful explanatory comments.
Suggested article: "People who claim code can explain itself considered harmful"
For a structured version of ^ that, see Go's defer.
int foo(int v) {
// ...
int something = 0;
switch (v) {
case FIRST_CASE: something = 2; goto common1;
case SECOND_CASE: something = 7; goto common1;
case THIRD_CASE: something = 9; goto common1;
common1:
/* code common to FIRST, SECOND and THIRD cases */
break;
case FOURTH_CASE: something = 10; goto common2;
case FIFTH_CASE: something = 42; goto common2;
common2:
/* code common to FOURTH and FIFTH cases */
break;
}
}
The D version: int foo(int v) {
// ...
int something = 0;
void common1() {
/* code common to FIRST, SECOND and THIRD cases */
}
void common2() {
/* code common to FOURTH and FIFTH cases */
}
switch (v) {
case FIRST_CASE: something = 2; common1(); break;
case SECOND_CASE: something = 7; common1(); break;
case THIRD_CASE: something = 9; common1(); break;
case FOURTH_CASE: something = 10; common2(); break;
case FIFTH_CASE: something = 42; common2(); break;
default: break;
}
}
Note the use of nested functions to factor out common code. The nested functions usually get inlined by the compiler, so there is no cost to them. Nested functions are a great way to eliminate gotos without penalty.This kind of code typically gets written when ‘usually’ isn’t good enough (of course, once you use a compiler, in theory, there are no guarantees; the compiler could compile the inlined-function one with a goto or vice versa, but programmers typically are more concerned about what happens in practice)
The inlined functions also may increase code size and instruction cache pressure.
On the other hand, having a branch less may be beneficial.
Straightforward and limited-scope code like nested functions tends to improve in performance over time, because it restricts possibilities better than goto. And it's more error-resistant to future changes for similar reasons. If your code has to last a while, you're probably better off having the safer one. Or maintain both, and use the safer one to validate the unsafe one, and choose based on benchmarks of the week - what was true when it was written could change with any version.
> The inlined functions also may increase code size and instruction cache pressure.
tail merging optimization takes care of that
They seem to generate a lot of hate though.
Of course, you probably should not have a global state that some clean up function with a side effect deals with in the first place, but it's the c linux kernel so I assume there is something I don't know.
But there is a good case to be made that exception handlers are gotos in the Dijkstra sense, at least when used for anything other than exceptions, like passing errors around.
> These retain structure and this are not considered harmful
This might be your opinion, and it is a very reasonable opionin. But it is just not what Dijkstra is arguing. He is very clearly arguing for “single entry single exit”.
Agreed. Return forces you into a single exit. Upon hitting return, the code can only return back to where the function was originally called. It cannot 'arbitrarily' jump to some other place in code as you could do in an unstructured programming language like, say, BASIC. Which is what Dijkstra was pushing for, being a strong proponent of structured programming.
I don't know of any modern programming language that does allow anything outside of a single exit, exception handlers and setjmp/longjmp excepted. There is a good case to be made that the latter two reintroduce the very problem Dijkstra warned of and are generally considered harmful for the same reason.
Exiting in the middle of a block would be just as bad as entering in the middle, according to the argument he is making.
However, Dijkstra accepts abortion clauses, which is what return really is (it is not an exit clause). My take is that his argument is that an unbridled go to is too primitive and that he believed go to statements should be bridled by additional structure that help describe the process, not that go to should be avoided entirely.
While I think we can agree that return is go to, it is a bridled go to. It strictly limits what a programmer can do, avoiding the mess Dijkstra claims an unbridled go to promotes. It is predictable and understandable, clearly describing the intent.
Exception handlers have no such strictness. It is not clear, without studying the program in its entirety, where your code will end up. That can be a good tradeoff when you are dealing with exceptions. The only reasonable response to encountering an exception is to ultimately crash, so at that point who cares? But, indeed, using exception handlers for control flow (e.g. passing errors around) is considered harmful.
It is not my view. I happen to disagree with Dijkstra on that point.
do {
r = allocate_resource();
if (!r)
break;
} while (false);
When I've said that this is just a messy unreadable version of goto, I was told that goto is harmful and this is a structured programming, which is superior. GOTOphobia is real.Lots of people just follow whatever anyone, who they perceive as authority, says. Critical thinking isn't a common trait.
This person is living in a pretend bubble that isn't grounded in the reality of large projects, multiple team members, deadlines, changing requirements, etc.
No programmer is perfect. And when your tool can cut your arm off, you should be careful or route around the dangerous bits when possible.
Maybe it's some kind of defencive response against newer programing languages slowly eating up spaces C used to be dominant in (like command line tools and system daemons), maybe it's just the programmer saying this thinking they're that special. Either way, most people saying this are setting themselves up for failure.
I wouldn't start a new project in C unless I absolutely have to but if you disagree, you can at least admit that there are dangers in C that you need help with if you want to be sure you're doing everything right. With most warnings treated as errors, extended warnings enabled, linting to make things like missing brackets obvious, static analysis of all code to spot difficult bugs, automated dynamic analysis of test cases and a proper testing pipeline I believe you can write C code that's safe enough.
In this case, I'm willing to give the authors the benefit of the doubt because goto hatred is worse than the risk posed by goto in most settings. Gotos used right are fancy if/switch statements and avoiding them in C can lead to a mess that doesn't add much safety. Most examples given are better in my opinion, because using goto can replicate the code flow modern languages provide with things like when/match/defer keywords.
I learned to appreciate C++ RAII already on Turbo C++ for MS-DOS around 1993, and using raw C has always been because I was required to deliver C code for some university projects or work.
When was the last time you did “cut your arm” with goto specifically? What’s the count and time ratio to other issues? Were these also addressed as taboo or left as “experience earned”? Gotophobia in its largest part is just a stupid meme with no real world data.
What an oddly specific presumption.
Even 30% safer is a win.
30% safer, 30% more readable, and 30% more productive would be even better.
> When was the last time you did “cut your arm” with goto specifically?
It's been a while since I've used C, and even longer since I've personally written goto statements. I do remember frequently getting tripped up on them right after undergrad. It's not friendly, and I don't ever wish to touch them again.
I'm working in a C++ game engine project right now and it's constantly segfaulting. I can't imagine that setting register jumps manually in complex higher level code would improve my situation.
When I get to choose the language, I use Rust. It fits the C use case and fixes many of the warts.
So it’s something bad from the undergrad past, no details. Must we take an advice based on that? I’m not sure I will.
I'm working in a C++ game engine project right now and it's constantly segfaulting. I can't imagine that setting register jumps manually in complex higher level code would improve my situation.
Neither would tabooing something based on weak or no evidence. “It doesn’t help here” and “we ban it and ostracize its use” are two different claims.
I was writing laser projector video games that played on the side of skyscrapers in undergrad. Speak for yourself.
Multiple COMEFROMs referencing the same label are an interesting way to effect multi-threading!
I like you!
Personally, I use guards heavily to reduce nesting and if the language supports goto's, I'll use them if it make sense to improve code flow. However, its been a very long time since I've needed to use goto :)
We make an exception for this at work for checks at the start of a function (eg. NULL, ranges, etc.) and it tends to save an indentation or two. Then generally still follow the single return rule otherwise.
0:[0226:084752]:sun-go:~/txr$ git grep '\<goto\> ' '*.c' '*.h' '*.l' '*.y' | wc
402 1386 12866The problem was not goto, it was that the else path was also “give up”. In a non goto/RAII/defer language it would have been something like
If (error)
Return
Return
My assumption has been this was some kind of merge error rather being wrong off the bay. Interestingly mandatory indenting or mandatory braces might have stopped this, but then i would have thought -Werror with the dead code warnings would have as well :-/But again the error was not the goto, and believing it was is the exact problem the article is talking about: people are so opposed to goto they are unable to see real issues (I have seen C code that tries to avoid goto completely and error handling code becomes horrific, far more complex, and far more error prone that just using goto). The problem with goto is that it is very easy to use it unnecessarily, in ways that complicated control flow but don’t actually make things better.
Many people seem to really dislike using the brackets but this is what that gets you.
Interestingly (or perhaps coming full circle), the bug reference by the GGP comment is also called out in the GCC 6.1 release notes:
> -Wmisleading-indentation warns about places where the indentation of the code gives a misleading idea of the block structure of the code to a human reader. For example, given CVE-2014-1266
sslKeyExchange.c: In function 'SSLVerifySignedServerKeyExchange':
sslKeyExchange.c:629:3: warning: this 'if' clause does not guard... [-
Wmisleading-indentation]
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
^~
sslKeyExchange.c:631:5: note: ...this statement, but the latter is misleadingly indented as if it is guarded by the 'if'
goto fail;
^~~~Which is most frightening to me.
Pyhon would have prevented this bug, but so would a formatter. Rust also requires braces for if-blocks to prevent this kind of error.
Using a language feature that is a known footgun (`if ...` instead of `if {...}`) without being cautious enough to avoid shooting yourself in the foot is not the fault of the footgun, it's the fault of the programmer.
Additionally, in the above linked case, the problem isn't a misused `goto`, it's a misused `if ...`. It would be just as problematic if they typed `cleanup_context();` instead of `goto fail;`, but nobody complains about cleaning up state, do they?
additional variables
Are we still chasing down the least variables possible in 2023?
BUT, the fact they have multiple goto locations in one function violates this! Only one goto locations ! That goto is goto cleanup, or goto exit. What you do is then check state of each variable you cleanup. Every function should be some variable of this. If anyone writes C in any other style than SESE, you can consider them a subpar C programmer. There's variations like using BOOL and in and out variables. I like them, but there are different styles. But anyone not using a single AND ONLY A SINGLE goto in every function is 100% a subpar C programmer who you should not trust.
BOOL foo()
{
int *allocation;
char *allocation2;
BOOL bRet = FALSE;
const int BUFFSIZE = 10; //NO MAGIC NUMBERS
allocation = resourceallocation(BUFFSIZE); // Malloc, file.open, network open, etc
if(!allocation)
{
DEBUGPRINT("ALLOCAITON FAILED");
bRet = FALSE; //Redundent, but protect against intern
goto cleanup;
}
allocation2 = resourceallocation2(BUFFSIZE); // Malloc, file.open, network open, etc
if(!allocation2)
{
DEBUGPRINT("ALLOCAITON FAILED");
bRet = FALSE; //Redundent, but protect against intern
goto cleanup;
}
...
bRet = TRUE;
cleanup:
//Add error handling if allocation fails
if(allocation)
resourcefree(allocation);
if(allocation2)
resourcefree(allocation2);
return bRet;
}For a big example of substandard coding, see this thread for an egregious wireguard module in BSD. Countless other examples. https://news.ycombinator.com/item?id=33381949
or at least at the time it was written, there werent alternatives that were performant enough.
Each has its place. You don't 'need' to use Goto's at all. Your example could be achieved with more flags and if statements.
FYI:
4,879 code results in illumos/illumos-gate for goto
2,587 code results in freebsd/freebsd-src for goto
It's not like any comparable project is immune? Perhaps `goto` says more about how old the code is?