On Using Guards in C++
Early return statements are a controversial topic across many programming languages. Some people find that they improve readability because they avoid carrying a result variable down the end of a function. And some other people find they constitute a danger because they introduce complexity: with them, a function suddenly has several exit doors.
Today I want to focus on a special type of early return statements: guards. Guards consist in early return statements placed at the beginning of a routine for handling special cases. And seeing how to use guards will get us to a more general concept for code readability: symmetry in code, which has also a lot to do with nesting.
While all this is valid in other programming languages that have at least some procedural aspects, such as Java, C# and many others, we’ll finish with a question that’s very specific to C++: do guards have an impact on the Return Value Optimization?
Use guards for breaking a symmetry
Before going further, what’s a guard exactly?
A guard is a test at the beginning of a function (or a method) that deals with special cases, generally error cases, and cuts off the execution of a function to immediately return a result.
Here is an example: this is a function that applies a discount on an item in order to get is selling price. But the item may not be available.
double computePrice(Item const& item) { if (!isAvailable(item)) return 0; // this is the guard return applyDiscount(item.getPrice()); }
In fact you don’t need to write them on one line, you can even uses braces {} as with if statements in general. But I like this way because it reads like the specification: “if the item isn’t available, return 0.” And as we saw earlier, if statements should do their best to look like their specifications.
Note that I’ve stripped off any sort of error management on purpose here, because this is not the point of this article. Indeed, the code could throw an exception instead of returning 0, or it could return an optional, and we could also use a domain object to represent the price:
std::optional<Price> computePrice(Item const& item) { if (!isAvailable(item)) return std::nullopt; // this is the guard return applyDiscount(item.getPrice()); }
But let’s keep the focus on the control flow here.
Another way to write the code, without guards, is this:
double computePrice(Item const& item) { if (isAvailable(item)) { return applyDiscount(item.getPrice()); } else { return 0; } }
But the code without guards has the following drawbacks:
- it has deeper nesting,
- it has are more technical components showing concerning its control flow,
- if there were more than one special case, it would need additional
else if
branches, whereas the guards would just need one more line per case.
Those three issues make it a bit harder to read than the version with guards. But there is one even bigger aspect that makes the version using guards more expressive, and that is how I think we should use guards: the version using guards breaks the symmetry.
Indeed, the second version, the one without guards, shows some symmetry: the two return statements are in the if
and the else
branch of the if statement. Even the physical aspect of the code is symmetric: both return statements have the same level of indentation, and they are both surrounded with aligned braces.
But in this case this symmetry is misleading. Indeed, according to our definition, guards are made to handle special cases. And special cases should look different than the main portion of a routine. Guards achieve just that! By packing the special cases in a dedicated place, a glance at the code lets you see that the function has two very different parts.
And this gets even more true if there are several special cases. Compare this code using guards:
double computePrice(Item const& item) { if (!isAvailable(item)) throw ItemNotAvailable(item); if (!isOpen(item.getStore()) throw StoreClosed(item); if (!shippingAuthorized(item)) throw ShippingNotAuthorized(item); return applyDiscount(item.getPrice()); }
with this one, that doesn’t use guards:
double computePrice(Item const& item) { if (!isAvailable(item)) { throw ItemNotAvailable(item); } else if (!isOpen(item.getStore()) { throw StoreClosed(item); } else if (!shippingAuthorized(item)) { throw ShippingNotAuthorized(item); } else { return applyDiscount(item.getPrice()); } }
Even though both pieces of code have more contents containing special cases, the first one clearly shows where the 3 special cases are and where the main code is, while in the second snippet the main code looks like it is the one being special.
So use guards for breaking a symmetry that doesn’t make sense.
Don’t sacrifice symmetry to reduce nesting
One of the other advantages of guards is that they reduce nesting. And nesting is often a bad thing for expressiveness of code, because each level stacks up in our mental RAM until it causes a stack overflow. And we humans overflow way, way, way faster than our friends the computers.
But as often, we shouldn’t follow guidelines blindly (I guess this stands true for this meta-guideline too, so this means there must be guidelines that we should follow blindly?). And a pinch of nesting can be good, in particular to create symmetry.
What follows is very similar to what we saw on guards, but the other way around (thus creating a symmetry if you will, oh gosh this is getting so meta I’m not sure I can follow).
Consider the following piece of code:
double applyDiscount(double price) { if (reductionType_ == Percentage) { return price * (1 - reductionValue_); } else { return price - reductionValue_; } }
If the price reduction is in percentage, like 10% off the price, then applying the discount does a multiplication. Otherwise it subtracts the discount value, like 3$ off the price (and reductionType_
and reductionValue_
come from somewhere and Percentage
is something, it’s not the point here).
Now consider this other way to write that code:
double applyDiscount(double price) { if (reductionType_ == Percentage) { return price * (1 - reductionValue_); } return price - reductionValue_; }
This reduced the nesting of the lower part of the function, but I hope you can see that this second snippet is bad code. Why is it so? Because it broke a symmetry that made sense.
Indeed, the two types of reduction were equally valid: a discount could be either in percentage or in absolute value. There is no error case, or anything particular on either case. But the layout of the code says something different.
It looks like the percentage type has a different status because it is explicitly tested. As if it was a guard. Indeed, the function now reads like this: “if it’s percentage then do the multiplication, otherwise do the normal thing“, which is wrong!
Even though the generated binary code will likely be identical, the first piece of code states the intents of its author in a clearer way.
Will guards arrest the RVO?
Since guards introduce additional return statements in a function, will this prevent the compiler from applying the Return Value Optimizations?
In fact this question doesn’t oppose guards to the if-else statements we saw in the first section of this post, but rather to a more linear flow with a unique return statement at the end of the function. Like:
double computePrice(Item const& item) { if (!isAvailable(item)) return 0; // this is the guard return applyDiscount(item.getPrice()); }
versus:
double computePrice(Item const& item) { double price = 0; if (isAvailable(item)) { price = applyDiscount(item.getPrice()); } return price; }
Note that question is more specific than “do early return statements prevent the RVO”, because guards as we saw them returned unnamed objects constructed directly on the return statement. And this makes it easier for the compiler to optimize them away.
I’ve experimented by tweaking this code snippet in different directions, and the answer I found is that they don’t prevent the RVO, but they prevent the NRVO for the rest of the function. That is to say that both returned objects (on the guard and in the main part) benefit from the RVO. However the very presence of the guard in the function disables the NRVO for the object returned by the main part of the function.
I can’t guarantee that your compiler will produce exactly the same result, and even less that whatever optimisations your compiler does matter in this particular part of your code. But what I can say is that guards can have an impact on the NRVO, and it’s good to be aware of it to understand potential performance issues.
Note that, even when the NRVO is disabled move semantics are still active, but as Scott Meyers warns us in Item 29 of Effective Modern C++ we should assume that move operations are not present, not cheap and not used.
What about you?
Guards and early returns in general are topics where pretty much everyone has a strong opinion. What’s yours? You’re welcome to share how you use return statements to tame the control flow of your functions?
Related:
- How to Make If Statements More Understandable
- Do Understandable If Statements Run Slower?
- Return Value Optimizations
Share this post!