Jon Jagger
jon@jaggersoft.com

Single Exit

In CVu 15.4 Francis makes a case that some functions are less complex if they use multiple return statements. In Overload 55 I stated my preference for single exit via a single return. In this article I explore the examples Francis presented and explain my preference more explicitly.

Example 1 - Multiple Returns

The first code fragment Francis presented was as follows:

bool contains(vector<vector<int> > const & array, int value) 
{
    int const rows(array.size());
    int const columns(array[0].size());
    for (int row(0); row != rows; ++row) 
    {
        for (int col(0); col != columns; ++col) 
        {
            if (array[row][col] == value) 
            {
                return true;
            }
        }
    }
    return false;
}

And he wrote "if you are a believer that functions should never have more than a single return you have a problem because however you reorganise your code the requirement is for two distinct exit conditions".

I'm a firm believer, but even if I wasn't I'd have to agree that some multiple returns somehow feel more acceptable than others. And as Francis says "perceived complexity is a function of many things". However I don't think it's quite accurate to say the requirement is for two distinct exit conditions. To try and explain what I mean, consider the following implementation of contains [1]:

bool contains(const vector<vector<int> > & array, int value) 
{
    vector<int> all;
    for (int at = 0; at != array.size(); ++at)
    {
        copy(array[at].begin(), array[at].end(), 
            back_inserter(all));
    }

    return find(all.begin(), all.end(), value) != all.end();
}

This is an unusual implementation but it does show that the requirement is always to return a single value to the function caller (in this case either true or false). Exactly how you do so depends on your choice of implementation which is a different matter. Another approach would be to design an iterator adapter class that "flattens" the iteration through a container of containers.

Francis continues "The only ways these can be combined in a single return statement require either continuing processing after you know the answer or increasing the perceived complexity of the code." Here is the heart of the issue - the complexity of the code. Is a single-return version of contains necessarily more complex?

Example 1 - Single Return

Here's a more realistic single-return version of contains:

bool contains(const vector<vector<int> > & values, int value) 
{
    int at = 0;
    while (at != values.size() && !exists(values[at], value))
    {
        ++at;
    }
    return at != values.size();
}

This makes use of the following non-standard helper function:

template<typename range, typename value >
bool exists(const range & all, const value & to_find)
{
    return find(all.begin(), all.end(), to_find) != all.end();
}

Example 1 - Comparison

What are the differences between these single/multiple return versions?

Example 2 - Multiple Returns

The second code fragment Francis presented is as follows (some code elided, I assume the int/bool conversions are deliberate):

bool will_be_alive(life_universe const & data,
                   int i, int j)
{
    int const diagonal_neighbours = ...;
    int const orthogonal_neighbours = ...;
    int const live_neighbours =
        diagonal_neighbours + orthogonal_neighbours;

    if (live_neighbours == 3) return true;
    if (live_neighbours == 2) return data[i][j];
    return false;
}

I would start by rewriting the end of this function as follows:

    if (live_neighbours == 3) 
        return true;
    else if (live_neighbours == 2) 
        return data[i][j];
    else
        return false;

The difference is the explicit coding of the control-flow surrounding the return statements. Do you think making the control-flow explicit is a good thing? If you're not that bothered I invite you to consider the following:

    if (live_neighbours == 3) 
    return true;

I hope you're more concerned by this lack of indentation. These days indenting your code to reflect logical grouping is taken as an article of faith and people don't question or recall exactly why it is used. Indentation visibly groups similar actions and decisions occurring at the same level. If you believe that indentation is a Good Thing there is a strong case for clearly and explicitly emphasise that all three return statements exist at the same level. In contrast, and significantly, the multiple-returns in the first example are not at the same level.

Example 2 - Single Return

Francis also presented example 2 using a single return involving a nested ternary operators:

    return (live_neighbours == 3) 
       ? true
       : (live_neighbours == 2)
           ? data[i][j]
           : false;

I agree with Francis that this adds nothing in terms of clarity. In fact I think it's a big minus. This is the kind of code that gives the ternary operator a bad name. But inside this long and inelegant statement there is a shorter and more elegant one trying to get out. To help it escape consider a small progression. Start with this (not uncommon) pattern:

    bool result;
    if (expression)
        result = true;
    else
        result = false;
    return result;

This is exactly the kind of code that gives single-exit a bad name. It is overly verbose; it isn't a simple, clear, and direct expression of its hidden logic. It is better as:

    if (expression)
        return true;
    else
        return false;

But this is still overly verbose. So we take a short step to:

    return (expression) ? true : false;

And removing the last bit of duplication we finally arrive at:

    return expression;

This is not better merely because it is shorter. It is better because it is a more direct expression of the problem. It has been stripped of its solution focused temporary variable, its if-else, and its assignments; all that remains is the problem focused expression of the answer. It has less code and more software. Applying the same process to the chained if-else containing three return statements we arrive not at a nested ternary operator but at this:

    return live_neighbours == 3 ||
              live_neighbours == 2 && data[i][j];

This is focused on and is a direct expression of the problem in exactly the same way.

Conclusion

My rules of thumb are as follows:

But remember, dogmatically following rules is not a recipe for good software. The best software flows from programmers who think about what they do and who follow principles and practices that naturally generate quality.

Many thanks to Kevlin for an insightful review of a first draft of this article.

That's all for now.

{ JSL }
Jagger Software Ltd
Company # 4070126
VAT # 762 5213 42