(Update below)
At the time of writing this, the Mozilla Coding Style guidelines have this recommendation under “General C/C++ Practices”:
Don’t put an else right after a return. Delete the else, it’s unnecessary and increases indentation level.
I can appreciate this in some circumstances, such as when checking for an error case early in a long function:
void f() { if (errorcase()) return; a(); b(); c(); d(); }
But as a blanket guideline I think it’s misguided because it promotes an impure style of programming. And by “impure” here I mean the opposite of “pure” in this sense, i.e. “free from side-effects and other such error-prone junk”. Let’s consider a simple example: the max() function. Here’s a way to write it that is quite sensible, in my opinion:
int max(int a, int b) { if (a > b) return a; else return b; }
But this violates the no-else-after-return guideline. The Mozilla way to write this is like so:
int max(int a, int b) { if (a > b) return a; return b; }
(I’m not exaggerating, see this comment and this comment from Mozilla’s VP of Engineering and CTO respectively, about an example very similar to this.)
Urk. That’s horrible. Any time you’re using an ‘if’ without a corresponding ‘else’, you’re relying on an impure feature — you must be doing something with a side-effect in the ‘then’ branch, or some kind of impure control flow. Mozilla’s coding guidelines insist that we write max(), a pure function, in an impure way. Also, it’s just harder to read because the two return statements have different indentation levels despite the fact that they are closely related.
In this case there’s a way around the problem:
int max(int a, int b) { return (a > b ? a : b); }Even though C’s if-then-else expression syntax is awful, it’s a better way to do it.
In general, statements suck, expressions rule. Even in a language like C/C++ you can do something approaching functional programming by avoiding statements where possible. In a tiny example like max() the difference may seem trivial, but in bigger examples the difference becomes more important. Consider the following code from Nanojit’s register allocator which picks a register by looking at the free registers, the registers that are allowed to be used, and possibly the preferred registers. You don’t have to understand it closely, but it helps to know that RegisterMask is just a bitfield with one bit per register.
Register Assembler::registerAlloc(RegisterMask allow) { RegAlloc ®s = _allocator; RegisterMask prefer = SavedRegs & allow; RegisterMask free = regs.free & allow; RegisterMask set = prefer; if (set == 0) set = allow; if (free) { // at least one is free set &= free; // ok we have at least 1 free register so let's try to pick // the best one given the profile of the instruction if (!set) { // desired register class is not free so pick first of any class set = free; } NanoAssert((set & allow) != 0); Register r = nRegisterAllocFromSet(set); regs.used |= rmask(r); return r; } // ... more code here ... }I find this code incredibly hard to understand. There are three ‘ifs’ that lack ‘elses’, so it relies heavily on state updates to do its work. In fact, in order to understand it, I ended up rewriting it in a more pure style, by using a sequence of small refactorings:
Register Assembler::registerAlloc(RegisterMask allow) { RegAlloc ®s = _allocator; RegisterMask allowedAndFree = allow & regs.free; if (allowedAndFree) { // At least one usable register is free -- no need to steal. // Pick a preferred one if possible. RegisterMask preferredAndFree = allowedAndFree & SavedRegs; RegisterMask set = ( preferredAndFree ? preferredAndFree : allowedAndFree ); Register r = nRegisterAllocFromSet(set); regs.used |= rmask(r); return r; }// ... more code here ... }There’s now only a single ‘if’ lacking an ‘else’, and it turns out even that can be removed by putting the “… more code here …” part into an else branch, and then the ‘return r;’ can be moved to the end of the function.
The code is now comprehensible — it picks a register that is free and allowed, and preferred as well if possible.
Part of the art of programming involves knowing when to avoid using a language feature. Everybody knows that ‘goto’ is occasionally highly useful, but should be avoided whenever something less dangerous (a for-loop, a while-loop, a ‘break’ statement) can be used. And everybody knows it because everybody is taught it. But programmers aren’t taught to avoid unnecessary side-effects… at least, I wasn’t, certainly not via examples such as those I’ve given above. I’ve only come to realise these things as I’ve switched several times between using C/C++ and some pure functional languages over the years, and come to appreciate how purity helps make programs more readable and less error-prone.
To come full circle, I’d like to see the no-else-after-return guideline removed from the Mozilla style guide, or at the very least, be qualified in a way that allows the cases I’ve argued for. I’d also love to see a guideline (with examples) added such as “prefer expressions to statements”.
Update:
(The arguments had the wrong names in the initial post, I’ve fixed them now.)
All three versions of max() are pure, because it’s possible to combine impure pieces of code (such as an else-less if) into a pure piece of code. So I’ll augment my recommendation: pure code is preferable, and furthermore, given that C/C++ makes it so easy to write impure code, it’s preferable that pure code be obviously pure. I argue that the 3rd version of max() with the ?: operator is the most obviously pure, because a ?: is pure if its operands are pure. In comparison, it’s harder to tell if an if-then-else is pure (although this case is still pretty easy thanks to the matching return statements). And an if-without-else almost never results in pure code; the max() example above is a rare exception.
I also omitted a fourth formulation of max:
int max(int a, int b) { int m; if (a > b) m = a; else m = b; return m; }For this very simple case, such a formulation is overkill. But if the ‘then’ and ‘else’ cases are more complicated it makes more sense. (Furthermore, much of this discussion should be thought of in more complex cases, as these kinds of small decision can make a bigger difference then.) This version also doesn’t violate the no-else-after-return rule. So perhaps that rule isn’t as bad as I thought, if only because it’s easier to work around than I thought… but I still think rewriting the 1st version of max() into the 2nd version is a bad idea.