Suppose we want to write a function that returns a value, but with the possibility that the computation might fail. This failure can be represented as an exception, or as a return value that unamibiguously denotes failure (for example, -1). Throwing an exception is a strong form of failure and might not be appropriate in certain situations. Take the example of a lookup table that maps strings to integers. If the given key is in the table, then we can return the corresponding integer value. But what if the string is not in the table? This could be a common scenario in the application domain, and hence throwing an exception is too costly, besides complicating the processing logic. On the other hand, we cannot return something like -1 to indicate failure because the value associatd with the key could be an arbitrary integer! Of course, we can introduce an extra function argument to denote the status code, but this is not elegant. What we need in this situation is the ability to return a valid value, or to return something that denotes no value. std::optional<T> proves handy in such situations.
Consider the following example:
Here we define an optional variable to hold integers. This means the variable can, at any time, contain an integer value, or no value at all. As you can see from the variable definition, because there is no initial value for this variable, it is deemed to contain no value to start with.
How can we find out if the variable contains a value or not? There is a member function called has_value() that returns true if it has a value, or false if it doesn’t. Or, because there is an implicit conversion to bool, we can directly use the variable in a conditional expression.
As in the case of regular variables, we can assign a value to the optional variable and replace its current value, if any. So, in the above example, only the third conditional check prints the value.
Can we use optional with user-defined types? Yes, of course. Let us consider a simple struct X shown in the following figure:
The code fragment below shows how this is used with optional.
As in the first example, because the optional variable is defined using default initialization, the variable is deemed to be empty. However, when we provide an explicit initial value for the variable opt_val2, the optional holds that value.
The next example shows different ways to get the value of the optional variable. We can use the value() member function, use direct assignment, or use the smart pointer dereferencing mechanism.
Recall our initial discussion about a function returning a valid value or no value at all. To simplify the usage of such functions, there is a member function called value_or() that allows us to supply a default value to use in case the function returns no value. This is illustratd in the following example:
Notice how the getValue(string) function returns std::nullopt to denote no value. The next code fragment uses value_or() to return 9999 if getValue() happens to return no value. This allows us to write much cleaner code.
What if we want an optional variable that currently holds a value to become empty (i.e. no value)? The reset() member function does just that. This function destroys the value in the optional (calls destructor for user defined types). This is shown in the example below:
Once reset, the optional has no value as the example illustrates.
In summary, std::optional<> gives us an elegant (and efficient) construct to represent the mutually exclusive value/no value phenomenon.
By the way, could we have used std::tuple<>, std::pair<>, or std::variant<> to model this situation? Think about it. More on this in a future article!
The example source code is here. This has been tested in Visual Studio 2017.
Have a wonderful day!
Recent Comments