Jonathan Boccara's blog

Clearer interfaces with optional<T>

Published November 24, 2016 - 2 Comments

Daily C++

The need for representing a value that is “empty”, “null”, or “not set” arises regularly in code but often leads to clumsy or brittle solutions.

This need may appear with a function that needs to return a value but may not be able to compute it in all cases. In this case, several solutions are encountered in code:

  • returning a “special value“, like -1 where a positive integer is expected, or “” where a string is expected. This is brittle, because -1 or “” may actually be meaningful values, now or later, or be set accidentally.
  • returning a boolean or an error code indicating whether the function has succeeded in computing the result, and the result is then passed through a function parameter:
    bool computeResult(Result& result);

    This is brittle AND clumsy, because nothing enforces that the caller check the boolean returned, and this overall leads to code that is painful to write and to read.

  • throwing an exception. This is good, but not always usable, because surrounding code has then to be exception-safe. Moreover, many teams don’t use exceptions in their code.

optional<T> offers an easy way out.

What is optional ?

For a given type T, optional<T> represent an object that can be:

  • either a value of type T,
  • or an “empty” value.

This way a new value is added to the possible values that T can hold, which avoids to sacrifice a true value of T (like -1 or “”) to represent a T that is “empty” or “not set”.

optional<T> can also be perceived as a T that is packaged with a bool that indicates whether or not the T should be considered as “empty” (this is actually how optional is actually implemented).
To use it, for the moment #include <boost/optional.hpp>, and use it as boost::optional<T>.
Currently, optional is only available in boost, but it is in the process of standardization for C++17. Other languages have this concept, like Haskell with the Maybe construct.

Constructing and using optionals

To construct an optional use:

  • boost::make_optional(value), boost::optional<T>(value) or directly value (using implicit conversion) to construct an optional with an object actually having a value, or
  • boost::none, or boost::optional<T>() (default construction) to construct an empty optional<T>.

To manipulate an optional, you can test if it represents an actual value by using its implicit conversion to bool, and then access its values with * or ->. For example:

boost::optional<int> optionalInt = ... // optional being returned from a function
if (optionalInt)
{
    std::cout << "the value is " << *optionalInt << "\n";
}
else
{
    std::cout << "no value set!" << "\n";
}

There is an order defined on optional<T> as soon as there is one defined on T. By convention, an empty optional is considered smaller than all other values. This does not have much meaning in itself, but it provides an order so that optionals can be used in sorted containers.

Note that even though optional shares some similarities with pointers (dereferencing, checking for a “null” value) optional does not model a pointer at all. optional has value semantics, not pointer semantics. For instance copying a optional copies the wrapped value, and comparing 2 optionals compares the wrapped values.

 

Using optional<T> to simplify interfaces

Returning an optional<T> avoids the clumsy and brittle situations considered at the beginning of this post.
As a concrete illustration, let’s write a new interface for finding an element in a vector:

boost::optional<std::vector<int>::iterator> find(const std::vector<int>& v, int target)
{
    std::vector<int>::const_iterator targetPosition = std:find(begin(v), end(v), target);
    if(targetPosition != end(v))
    {
        return targetPosition;
    }
    else
    {
        return boost::none;
    }
}

Here the function cannot guarantee to actually find the target value in the collection. With optional as a return type, it expresses that it may or may not return an actual position in the collection.

It would be used this way:

auto targetPosition = find(v, target);
if (targetPosition)
{
    // use *targetPosition
}

As opposed to, with the raw version:

auto targetPosition = std::find(begin(v), end(v), target);
if (targetPosition != end(v))
{
    // use targetPosition
}

The comparison with end(v) ends up being a level of abstraction too low, because we don’t want to deal with iterators here, as they are technical constructs at this layer of the stack. optional<T> rises the level of abstraction, making the code clearer and more expressive.

The raw version has some advantages in some cases though (when doing several algorithms in a row, and for the very rare performance critical sections), but in the more basic case of searching for a value, I find this is detrimental for readability.

Refactoring legacy code with optional<T>

In case you have lots of legacy code where default values like -1 or “” are used extensively, and you don’t want to change all of it at the same time, you can still replace the most critical parts of your code with optionals, and leave the rest unchanged.

For this use the get_value_or (simply value_or for C++17’s std::optional) method to bridge up the two. It takes a value and returns it if the optional object turns out to be empty.
For instance, let’s consider the following legacy function:

int oldF(); // by convention, oldF returns -1 as a no-value

You would rewrite it so that it benefits from optional to express the no-value, the following way:

boost::optional<int> newF();

And to limit the amount of code modified at the same time you can still keep the old function, and make it call the new one:

int oldF()
{
    return newF().get_value_or(-1);
}

To go further with optional, have a look at the post on Partial queries with optional<T>, that shows a concrete example of using optional as an function argument, to leverage on its “not set” semantic to provide a clear and precise interface.

 

Related articles:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed