How to Design Early Returns in C++ (Based on Procedural Programming)
Travelling back from ACCU conference a couple of weeks ago, one of the insights that I’ve brought back with me is from Kevlin Henney’s talk Procedural Programming: It’s Back? It Never Went Away. It’s surprisingly simple but surprisingly insightful, and it has to do with early return statements.
Early return statements are controversial in the programming community and, often, deciding whether a given early return is OK comes down to listening to how your gut is feeling about it.
In his presentation about how procedural programming is not just a memory from the past, Kevlin gives a guideline that will help our brain also take part in the decision process of judging an early return statement.
Consider the following two pieces of code that determine whether a year is a leap year:
Code #1:
bool isLeapYear(int year) { if (year % 400 == 0) { return true; } else if (year % 100 == 0) { return false; } else if (year % 4 == 0) { return true; } else { return false; } }
Code #2:
bool isLeapYear(int year) { if (year % 400 == 0) { return true; } if (year % 100 == 0) { return false; } if (year % 4 == 0) { return true; } return false; }
The difference between those two pieces of code is that Code #1 is based on if
/ else if
/ else
structure, while Code #2 has several if
s followed by a return
.
Now the question is: which of the two pieces of code is the most readable?
You may think that it is Code #2. After all it has less characters, and even less nesting. In fact, even clang and the LLVM project consider that Code #2 is more readable. Indeed, they even implemented a refactoring in clang-tidy called readability-else-after-return, that removes else
s after the interruptions of the control flow – such as return
.
But Kevlin Henney stands for Code #1 when it comes to readability, and draws his argument from procedural programming.
So what makes Code #1 more readable?
Leave your reader on a need-to-know basis
Essentially, the argument for Code #1 is that you need to know less to understand the structure of the code.
Indeed, if we fold away the contents of the if statements, Code #1 becomes this:
bool isLeapYear(int year) { if (year % 400 == 0) { ... } else if (year % 100 == 0) { ... } else if (year % 4 == 0) { ... } else { ... } }
The structure of the code is very clear. There are 4 different paths based on the year
, they’re independent from each other, and each path will determine the boolean result of the function (if it doesn’t throw an exception).
Now let’s see how Code #2 looks like when we fold away the if statements:
bool isLeapYear(int year) { if (year % 400 == 0) { ... } if (year % 100 == 0) { ... } if (year % 4 == 0) { ... } return false; }
And now we know much less. Do the if statements contain a return
? Maybe.
Do they depend on each other? Potentially.
Do some of them rely on the last return false
of the function? Can’t tell.
With Code #2, you need to look inside of the if statement to understand the structure of the function. For that reason, Code #1 requires a reader to know less to understand the structure. It gives away information more easily than Code #2.
I think this is an interesting angle to look at code expressiveness: how much you need to know to understand the structure of a piece of code. The more you need to know, the less expressive.
In the leap year example, the if blocks are one-line return statements, so you probably wouldn’t fold them away anyway, or maybe just mentally. But the differences grows when the code gets bigger.
The Double Responsibility of return
Here is another way to compare Code #1 and Code #2. In C++, as well as in other languages, the return
keyword has two responsibilities:
- interrupting control flow,
- yielding a value.
One could argue that this violates the Single Responsibility Principle, that stipulates that every component in code should have exactly One responsibility.
Note, however, that this is not the case of every language. For example Fortran, quoted by Kevlin, uses two different mechanisms to fulfil those two responsibilities (RETURN
only interrupts the control flow, while assigning to the name of the function yields a value).
Now if we focus on the second role of return
, that is yielding a value, and rewrite our function in pseudo-code to only show that value when possible, Code #1 becomes:
bool isLeapYear(int year) { if (year % 400 == 0) { true } else if (year % 100 == 0) { false } else if (year % 4 == 0) { true } else { false } }
We see that we’re only using One responsibility of return
: yielding a value. Code #1 helps return
respect the SRP.
Now if we do the same thing with Code #2, we can’t get rid of the interrupting responsibility of return
:
bool isLeapYear(int year) { if (year % 400 == 0) { return true; } if (year % 100 == 0) { return false; } if (year % 4 == 0) { return true; } false }
Only the last return
has only responsibility (yielding a value). The other return
s mix their two responsibilities: interrupting the control flow and returning a value. And mixing responsibilities is not a good thing in programming.
English food and food for thought
This was one of the insights that I took away when I attended the ACCU conference 2018, and that I wanted to share with you. It’s a simple example that wraps a deep reflection on several fundamental aspects of programming. If you weren’t at ACCU to taste the English food, here is at least some food for thought.
Thanks to Kevlin for reviewing this article. If you’d like to watch his ACCU conference talk in full, here it is.
Related articles
- On Using Guards in C++
- How to Make If Statements More Understandable
- Do Understandable If Statements Run Slower?
Share this post!