Let me ask an “ancient” question:
How to break
out of an if
statement?
if(cond) {
work1();
auto result = work1();
if(result is not good){
break; // <- how to approach this?
}
a();
lot();
of();
other();
jobs();
}
This is a very basic question, right at the beginning of your programming career, you might be taught to solve it like this:
if(cond) {
do {
finish();
the();
jobs();
break; // <- available
other();
work();
} while(false);
}
This solution uses a pass-through do-while
statement, which enables break
in the internal block. But don’t you think it indent too much? And it doesn’t seem very intuitive.
I mean, the codes inside the block are executed, if, and only if both a true
condition and an explicit false
condition (the while(false)
) are presented.
Another disadvantage is that, if you want to hide the while(false)
structure, you define a macro:
#define breakable_if_begin(C) if(C) do {
#define breakable_if_end() } while(false);
breakable_if_begin(cond)
work();
break;
nomore();
work();
breakable_if_end()
You must end the code block with another macro (say, breakable_if_end()
). This is just silly and ugly.
I’m not writing a BEGIN_MASSIVE_MAP()
or something, but only a tiny-piddling if
- and I want it breakable, that’s all.
If you look up in the stackoverflow
There had been plenty of questions asked in the old days, such as the canonical thread. Many people were telling you to use goto
. goto
is simple, and bad, we have already known. The BEST practice of goto
is to only use them in the case of breaking out of nested if
s.
Huh, what the funny. The notion is that “the best practice of goto
equals break
”, but not that “the best practice of approaching break
equals goto
”. And even “the BEST” down-through goto
is not the best, because the label is always declared AFTER the goto
statement, which is still counterintuitive.
Now the Invisible Gorilla shows up:
Have you ever thought, of using switch
?
Let me straightly show you the approach:
// Also Hosted on Compiler Explorer:
// https://gcc.godbolt.org/z/38GoKTexb
#include <cstdio>
#define b_if(cond) \
switch (1 & static_cast<unsigned>(cond)) \
case 1:
int main(int argc, char** argv) {
b_if(argc > 2) {
printf("ok\n");
break;
throw "NO!";
}
switch (1) // Note how weird it looks
case 1: {
printf("yes\n");
printf("it's really working\n");
break;
throw "never here";
}
}
You probably never saw this “gorilla stuff” before. I have searched the entire internet (in both English and Chinese), and couldn’t find anyone talking about this at all. I guess people rarely realize the switch
statement also has “the single statement form”.
Actually, there isn’t any “form” of statements, only types:
C++ includes the following types of statements:
- labeled statements;
- expression statements;
- compound statements;
- selection statements;
- iteration statements;
- jump statements;
- declaration statements;
- try blocks;
- atomic and synchronized blocks (TM TS).
The switch
is a selection statement
, defined as follows:
attr(optional) switch ( init-statement(optional) condition ) statement
Compared to the if
statement:
attr(optional) if constexpr(optional) ( init-statement(optional) condition ) statement
As we can see their syntax is almost identical. The trailing statement part of each is exactly the same. So if we can write
if(cond) work();
We should be able to write
switch(cond) work();
And yes, it’s totally legitimate. Except it won’t do anything nor be compiled to any codes, since no cases can be matched.
But appending a case
clause makes all the sense.
case
is alabled statement
, having the form:label : statement
where label represents the
case <constexpr>
part.The statement part again can be any type of statement, of course including the
compound statement
({}
, AKA “a block”).In a
compound statement
, there can be any number of statements.Remember, it’s still inside the
switch
statement, meaning thebreak
statement is also available.
Finally there is the most interesting fact: How to express else
?
Just write a default
. Like this:
switch(cond) case 1:{
puts("when ");
puts("true.");
default: // <- that's it
// all the follows are in the "else" routine
puts("when ");
puts("false.");
}
Yes, since it’s yet inside the switch
statement. Clear enough, isn’t it?
This little discovery surprised me about two things.
1. “The standard” may differ a lot from “what we learned”.
When we were pupils, we didn’t “learn by studying”, but “by impartation”. Although we read the books and acquire knowledge by ourselves, the content is selectively chosen and specifically presented by the author, which may be still far from its original appearance. Something may be ignored, something may be twisted or slightly different in details. We learned an impression, not the matter.
But a standard must be very concrete, very specific. The standard may be challenged by a variety of strange interpretations and corner cases during drafting. So the final result is always unambiguous and well-categorized, to distinguish the right behavior from others. Like, I was looking forward to checking “the syntax of switch statement”, but the standard tells me “there is no ‘isolated switch statement’, there is ‘selection statement’, ‘switch’ is one of them” and “all the selection statement have a uniform representation”.
2. “Gorillas” hide in everywhere.
The “invisible gorilla” refers to a famous experiment in psychology that vividly demonstrated how our focusing target can blind us to unexpected possibilities.
In the very first lesson of programming we had quickly realized that the switch
statement is used to achieve multiple purposes in handling/dispatching. Since then the concept of “multi-purpose” was solidified and burnt to our brains. We see the switch
is always followed by a pair of braces because “multi-purpose” means a bunch of statements are needed to get into different routines, thus it is.
But we never, or hardly question the assumptions, look beyond the familiar fact:
- Is the
switch
statement designed to be with a brace? - Why are there other statements not mandatory to be bound with braces?
- Does a “single-purpose”
switch
statement even make sense?
You cannot uncover blind spots without maintaining strong curiosity and scrutinizing beyond conventional thinking.
Conclusion
At the beginning, I was just looking for an elegant way simulating labeled break
to replace goto
s in my code. I eventually discovered that the familiar switch
structure has some obvious, yet no one noticed syntax and features, hiding behind the specification of the standards.
Even seemingly simple constructs we use routinely may conceal powerful capabilities, awaiting those willing to question the invisible gorillas obstructing our enlightenment.
By embracing the lessons from this journey, I believe it can help us to become not only better programmers but also more insightful thinkers.