Else Before If
Imagine yourself discovering a part of your codebase and, in the midst of your exploration, you’re coming across an if statement of an honourable stature, featuring an `if` branch, an `else if` branch, and an `else`.
As you’re approaching it with a mix of suspicion and curiosity, the if statement presents you its foremost part: its `if` branch. And the `if` branch says something like: “if we’re in such edge case, then here is how to deal with it”. So far, this doesn’t really tell you the main purpose of that if statement.
Moving on, the `else if` branch is just as mysterious: “now if we’re in that very crooked other situation”, it says, “then here is how to cope with it.” At this point this is all very confusing, because you still don’t know what this if statement is about, really.
And finally, the `else` branch comes as a relief, because it describes the main case. When you’ve understood it, then you can go back to the if
and the else if
part, and they start making sense because you now know what this code is about.
In code, the structure of that if statement looks like this:
if (edgeCase1) { // deal with edge case 1 } else if (edgeCase2) { // deal with edge case 2 } else { // this is the main case }
Don’t you find it weird to be presented edge cases first, and to have to wait until the end to understand what the main case is about? Has it ever happened to you to write or read code in this order?
Let’s try to think of how to present the main case first, and then the edge cases afterwards, to make the code easier to discover.
else
first, if
after
So we’d like to somehow rewrite the code to something like this:
normally { // this is the main case } unless (edgeCase1) { // deal with edge case 1 } unless (edgeCase2) { // deal with edge case 2 }
Of course, this won’t compile because there is no such keyword as normally
and unless
in C++, I have just made them up. But now that we have a target syntax, we can try to work our way towards it and get as near as possible.
Let’s flesh out this code example to make it actually do something:
std::string text; normally { text = "normal case"; } unless (edgeCase1) { text = "edge case 1"; } unless (edgeCase2) { text = "edge case 2"; } std::cout << textToDisplay << '\n';
How do we make this compliant C++?
One possible interface
I’m going to show one possible interface that tries to get closer to the above target. I’m sure we can do better, but my purpose here is to illustrate that it is possible to get closer to the above code.
Here is the resulting code:
std::string text; normally ([&text]() { text = "normal case"; }, unless (edgeCase1) ([&text]() { text = "edge case 1"; }), unless (edgeCase2) ([&text]() { text = "edge case 2"; })); std::cout << text << '\n';
As you can see, it uses lambdas to implement the branches of the structure. Let’s get to the implementation in more details.
One implementation
It starts with normally
, a function that takes several “branches” (at least one), each branch being a template class:
template<typename NormallyBranch, typename... UnlessBranches> void normally(NormallyBranch&& normallyBranch, UnlessBranches&&... unlessBranches) { auto considerBranch = ConsiderBranch{}; auto resultOfConsideringUnlessBranches = for_each_arg(considerBranch, unlessBranches...); if (!resultOfConsideringUnlessBranches.hasExecutedABranch) { std::forward<NormallyBranch>(normallyBranch)(); } }
It builds a function object called ConsiderBranch
, that examines every branch except the first (the normally
) one, and determines if its condition is satisfied.
The branches template classes expose an (implicit) interface that contains a .condition()
method that returns the value of the condition of the branch, and an .action()
method that executes the code of that branch.
Given those two methods in the interface, here is how ConsiderBranch
uses them:
struct ConsiderBranch { bool hasExecutedABranch = false; template<typename Branch> void operator()(Branch&& branch) { if (!hasExecutedABranch && std::forward<Branch>(branch).condition() == true) { std::forward<Branch>(branch).action(); hasExecutedABranch = true; } } };
So the ConsiderBranch
object examines the unless
branches one by one, until finding one that has a true
condition, and executes its code.
Back to the code of normally
, if the ConsiderBranch
object hasn’t executed any of the unless
branches,
if (!resultOfConsideringUnlessBranches.hasExecutedABranch)
then we execute the code that was in the first branch of the normally
:
{ std::forward<NormallyBranch>(normallyBranch)(); } }
And that’s mostly it: there is just a bit a boilerplate to instantiate the unless
branches with the word unless
:
template<typename Action> struct Unless_branch { public: Unless_branch(bool condition, Action action) : condition_(condition), action_(std::move(action)) {} void action() const { action_(); } bool condition() const { return condition_; }; private: bool condition_; Action action_; }; struct unless { public: explicit unless(bool condition) : condition_(condition) {} template<typename Action> Unless_branch<Action> operator()(Action&& action) { return Unless_branch<Action>(condition_, std::forward<Action>(action)); } private: bool condition_; };
If you’d like to see all the code together, you can find it here.
normally
, it works
normally
and unless
are not native C++ constructs, but they show that we can bend the language to show the main conditions first, and the edge case afterwards.
Do you think there is a need at all for putting else
before if
? Is this is twisting the language too much? What do you think of the interface of normally
/unless
? And of its implementation?
Please let me know your opinion in the comments below.
You will also like
- Dry-comparisons: A C++ Library to Shorten Redundant If Statements
- How to Make If Statements More Understandable
- Do Understandable If Statements Run Slower?
- How to Check If an Inserted Object Was Already in a Map (with Expressive Code)
Share this post!