8 ms·
I wish C/C++ had a labeled break construct, like JavaScript, Java, Rust, and other languages have. It's surprisingly powerful, while still remaining structured.
by sunfish 11y ago
I wish C/C++ had a labeled break construct, like JavaScript, Java, Rust, and other languages have. It's surprisingly powerful, while still remaining structured. I personally have enjoyed learning about it, and about just how rarely an actual goto is really needed.
- api 11y agoI've never had to use 'goto' in C++ except to break from a nested loop. In C++ labeled breaks would make 'goto' completely obsolete. In C it would still have use in the implementation of orderly error handling -- the pattern where you hand-implement exception handling in C by putting an on_error: label at the end of the function that is goto'd on error. The addition of some orderly construct for this in C would eliminate that case, leaving no real role for goto there either.
- jasode 11y ago>In C++ labeled breaks would make 'goto' completely obsolete. In 20 years of using C/C++, I've found one use for "goto" that's hard to substitute: simulating coroutines that yield to an outer context. (Similar to C# yield return). A "break label" gets you out of a loop. I needed to use "goto" to jump back into the middle of a loop to resume where the "coroutine" previously left off. The keywords "break/setjmp/longjmp" wouldn't have been substitutes for this particular use case.
- prutschman 11y agoI've used a switch statement essentially as a jump table for this purpose. I'm curious whether that would have worked, or if you were doing something different enough that you needed a goto.
- IsTom 11y agoI don't quite understand how it's not a setjmp/longjmp usecase. Isn't this exactly what longjmp is for?
- frivoal 11y agoA bitecode interpreter is another place where it's nice to have gotos. Here's the base code without gotos: typedef enum { ADD, MUL, ..., END } opcode; void run() { opcode ins; while (1) { ins = fetch_next_inst(); switch (ins) { case ADD: perform_addition(); break; case MUL: perform_multiplication(); break; ... case END: wrap_up(); return; } } } You have 3 jumps on each loop. From the break to the end of the loop, then from the end to the top, and one from the switch to the right case. The first one might be optimized away, but let's remove it explicitly. typedef enum { ADD, MUL, ..., END } opcode; void run() { opcode ins; start: ins = fetch_next_inst(); switch (ins) { case ADD: perform_addition(); goto start; case MUL: perform_multiplication(); goto start; ... case END: wrap_up(); return; } } Assuming a non lousy compiler, we haven't improved anything yet. But now the fun starts. We can go down to one jump for each iteration. typedef enum { ADD, MUL, ..., END } opcode; #define NEXT() \ do { \ ins = fetch_next_inst(); \ goto *jump_table[ins]; \ } while(0) void run() { opcode ins; static void *jump_table[] = { &&add_l, &&mul_l, ..., &&end_l }; NEXT(); add_l: perform_addition(); NEXT(); mul_l: perform_multiplication(); NEXT(); ... end_l: wrap_up(); return; } Voila! a single jump every time around. Now, depending on what kind of architecture you're running on, the size of the cache, etc, this may or may not be faster. Granted, this is not the kind of code you write everyday. But sometimes speed matters, and good luck writing this without gotos.
- Niten 11y ago> The addition of some orderly construct for this in C would eliminate that case, leaving no real role for goto there either. I like the way this is handled in Go with the defer keyword: https://blog.golang.org/defer-panic-and-recover https://blog.golang.org/defer-panic-and-recover This construct gives you most of the power of C++ RAII without the overhead. Except that you can't use it to cleanup resources after exiting an anonymous block—it strictly defers to function exit.
- pcwalton 11y agodefer has unavoidable runtime overhead due to its dynamic semantics. It's strictly slower than RAII as implemented in C++ or Rust.
- giovannibajo1 11y agoThat's not true. The only case where the runtime overhead is unavoidable is calling defer in a loop, because it might require allocating the defer chain in the heap; in all other cases, it's just a metter of writing the correct optimization passes in the compiler.
- pcwalton 11y agoYes, that's what I meant by "strictly slower". At best, it can be optimized to something similar to what RAII can give you. RAII never has the overhead of the bad case.
- giovannibajo1 11y agoI don't want to be excessively picky, but you said that is has "unavoidable runtime overhead", and that's true only in its rarest form (defer within a loop), which is a feature which you can't implement in RAII. IOW, defer is a superset of RAII. In all other cases (which is almost all usages), it is semantically equivalent to RAII, so the language doesn't force any runtime overhead. The only difference is that the compiler is less mature than an average C++ compiler, but this is an implementation problem, not a design problem. RAII has no overhead because it is a pattern designed within the context of a zero-overhead language. defer allows you to implement a superset of cases that RAII handles, including those with runtime overhead.