An Attempt to Write Fallbacks With Expressive Code
When you need to initialise a value out of several possible choices and take the first valid one, the code can get verbose pretty quickly.
Let’s take an example, inspired from a piece of legacy code I saw once. We start with a simple case, where we need to assign a value from one specific source:
auto myResult = getTheValue(getTheSourceOfTheValue(myPieceOfData));
Let’s assume that getTheValue
returns an int
. And that if this int
is equal to 0
, then we’d like to try and fetch the result from somewhere else:
auto myResult = getTheValue(getTheSourceOfTheValue(myPieceOfData)) != 0 ? getTheValue(getTheSourceOfTheValue(myPieceOfData)) : getAnotherValue(getAnotherSourceForTheValue(myOtherPieceOfData));
And just for fun, let’s add another possible fallback:
auto myResult = getTheValue(getTheSourceOfTheValue(myPieceOfData)) != 0 ? getTheValue(getTheSourceOfTheValue(myPieceOfData)) : getAnotherValue(getAnotherSourceForTheValue(myOtherPieceOfData)) != 0 ? getAnotherValue(getAnotherSourceForTheValue(myOtherPieceOfData)) : getLastTrialOfTheValue(getTheEmergencySourceForValues(lastPieceOfData));
This is probably one of the worst ways to write this logic, because it is hard to read and has repetitions. But you can get across this pattern in legacy code. And I’ve chosen long and weird names on purpose to illustrate the example, because legacy code can have long and funky names.
Let’s see the various ways we can rewrite this piece of code to make it more expressive. This analysis is not meant to be definitive and comprehensive, but rather exploratory and the starting point for a discussion. Don’t hesitate to leave a comment below.
A simple solution
Simple solutions are often the best, so let’s start with one of the most simple ways to turn this expression around into readable code: spreading the logic across several lines to initialise each potential result:
auto const mainValue = getTheValue(getTheSourceOfTheValue(myPieceOfData)); auto const otherValue = getAnotherValue(getAnotherSourceForTheValue(myOtherPieceOfData)); auto const lastValue = getLastTrialOfTheValue(getTheEmergencySourceForValues(lastPieceOfData)); auto myResult = mainValue != 0 ? mainValue : otherValue != 0 ? otherValue : lastValue;
The advantages of this way is that we put a name over each sub-expression, to replace their lengthy and awkward naming by something more concise in our context, and that we removed the code duplication.
The drawback is that now, all three subexpressions get always invoked, whereas before they got invoked (granted, sometimes twice) only if the previous value weren’t valid. We also have more assignments, because we introduced intermediary objects. With int
that shouldn’t matter in terms of performance though, but in the general case this could matter.
However, the drawbacks are related to performance and, as always with performance, it is secondary to code clarity unless a profiler reveals this particular code is indeed causing a slowdown of the application.
Using optionals
If you have control on the functions that return the values, you could consider changing their prototype: if they can return an “invalid” value, here 0, this would probably best be represented with an optional
.
But before making that change, make sure that the special value is invalid in general, and not specifically in this context. Indeed, perhaps another context can find that 0 is an acceptable value.
Let’s assume that 0 means “invalid” in all contexts. The interfaces of getTheValue
and getAnotherValue
look like this:
int getTheValue(Source const& source); int getAnotherValue(OtherSource const& source);
We can introduce an optional
the following way:
std::optional<int> getTheValue(Source const& source); std::optional<int> getAnotherValue(OtherSource const& source);
This uses C++17’s std::optional
. Before C++17 we can use boost::optional
from the Boost Library.
optional
has a nice method that helps implementing a fallback: value_or
(in Boost, this is called get_optional_value_or
, and it is a free function). This lets us write the fallback mechanism this way:
auto const mainValue = getTheValue(getTheSourceOfTheValue(myPieceOfData)); auto const otherValue = getAnotherValue(getAnotherSourceForTheValue(myOtherPieceOfData)); auto const lastValue = getLastTrialOfTheValue(getTheEmergencySourceForValues(lastPieceOfData)); auto myResult = mainValue.value_or(otherValue.value_or(lastValue));
Wrapping the fallback behind an interface
But if you don’t have control on the interface, or if it doesn’t make sense to put an optional
because 0 is only invalid in our context, it would be nice to still be able to wrap the fallback mechanism behind an interface.
What follows is an attempt of implementing such a wrapping interface. In the end I prefer the first solution as it was only relying on standard C++ syntax. But the following was instructive to write and can be the basis of a discussion to improve it.
Let’s decide on the ideal interface (as we always do before starting the implementation): we need to specify how to determine if a value needs a fallback (here, being equal to 0), and we need to pass the list of candidate values.
So the calling code could look like this:
auto isInvalid = [](int n){ return n == 0; }; fallback(isInvalid, mainValue, otherValue, lastValue)
Given this interface, how can we implement fallback
?
Implementing fallback
fallback
can take an arbitrary number of parameters, so we’re going to use variadic templates. When working with variadic templates we should try to avoid recursion in general, because that can make long recursive stacks and result in a slower compilation.
In this case though, I couldn’t find how to avoid recursion. One classical way to avoid recursion is to use fold expressions, but there are not fold expressions with the ternary (?:
) operator.
So let’s go for the recursive solution, assuming that there can’t be that many fallback values anyway. If you see how to rewrite this code in a non-recursive way (a challenge for you, Seph? 😉 ) please leave a comment!
The general idea of the solution is to check if the first parameter needs a fallback. If it doesn’t, then we return it. Otherwise, we call fallback
again with the rest of the parameters:
template<typename ShouldFallBackPredicate, typename T, typename... Ts> T fallback(ShouldFallBackPredicate shouldFallBack, T&& value, Ts&&... fallbacks) { if (!shouldFallBack(value)) { return std::forward<T>(value); } else { return fallBack(shouldFallBack, std::forward<Ts>(fallbacks)...); } }
The recursion has to stop somewhere, when there is only one possible value. This is then the only value to return. We implement this case with an overload of the fallback
function:
template<typename ShouldFallBackPredicate, typename T> T fallback(ShouldFallBackPredicate, T&& value) { return value; }
Other solutions
These were three solutions to rewrite the initial awkward piece of legacy code.
Can you see how to improve those techniques?
How would you have proceeded if you had encountered such a piece of code in your code?
You will also like
- How to Make If Statements More Understandable
- Pointers, References and Optional References in C++
- Make your functions functional
- Functional Programming Is Not a Silver Bullet
- Clearer interfaces with optional<T>
Share this post!