Modern C++ Features – std::optional

Contents

Sometimes we want to express the state of “nothing meaningful” instead of a value. This is the use case for C++17’s std::optional.

In programming, we often come across the situation that there is not always a concrete value for something. For example, give me the first even number in a text, if there is any. If not, that’s fine. Or a class has an optional member, i.e. one that does not always need to be set.

In older code, these situations typically are solved by either “magic values” or null pointers. A magic value could be, for example, an empty string, or 0 or -1 or a maximum unsigned value, like std::string::npos.

Both approaches have their drawbacks: A magic value artificially constrains the range of values available. It is also only by convention distinguishable from valid, normal values. For some types, there are no obvious magic values, or values cannot be created in a trivial manner. A null pointer to denote no value means, that valid values have to be allocated somewhere, which is either a costly operation or difficult to implement.

Another approach is to provide two queries: First, ask if there is a meaningful value, and only if the answer is positive, ask for the value. Implementing this can lead to a needless repetition of lookup code, and the use is not safe. If asked for a value that is not there, the implementation of the second query has to do something. It can either return a garbage value that will be misinterpreted, invoke undefined behavior, or throw an exception. The latter usually is the only sensible behavior.

std::optional

C++17 introduces std::optional<T> into the standard library. Like std::variant, std::optional is a sum type. In this case, it’s the sum of the value range of T and a single “nothing here” state.

The latter has a name: its type is std::nullopt_t, and it has a single value std::nullopt. If that sounds familiar: It is the same concept as nullptr, with the difference that the latter is a language keyword.

Using std::optional

std::optional has pretty much all the features we would expect: We can construct and assign from any values that are convertible to T. We also can construct and assign from std::nullopt and default-construct to get an empty optional. We can construct and assign from std::optional of other types, if the two types are convertible as well. The result will contain the converted value or be empty, just like we would expect.

We can query a std::optional as described above: has_value() tells us whether there is a value, value() returns the value. If there is no value and we call value() anyway, an exception of type std::bad_optional_access is thrown. Alternatively, we can use value_or(U&& default) to get either the value, or the default, if the std::optional is empty.

int main()
{
  std::string text = /*...*/;
  std::optional<unsigned> opt = firstEvenNumberIn(text);
  if (opt.has_value()) 
  {
    std::cout << "The first even number is "
              << opt.value()
              << ".\n";
  }
}

In addition to those explicit methods, std::optional also provides an interface similar to smart pointers: It is explicitly convertible to bool to denote whether it contains a value. The pointer dereferencing operators * and -> are implemented, but without the std::bad_optional_access – accessing an empty std::optional this way is undefined behavior. Lastly, reset() destroys the contained object and makes it empty.

The above code can thus be rewritten as

int main()
{
  std::string text = /*...*/;
  std::optional<unsigned> opt = firstEvenNumberIn(text);
  if (opt) 
  {
    std::cout << "The first even number is "
              << *opt
              << ".\n";
  }
}

To round things off, std::make_optional can, analogous to std::make_unique and std::make_shared construct the T inside a new std::optional in-place, and the emplace(Args...) method can do the same to an existing std::optional.

auto optVec = std::make_optional<std::vector<int>>(3, 22); //{22, 22, 22}
std::set<int> ints{4, 7, 1, 41};
optVec.emplace(std::begin(ints), std::end(ints));
std::copy(optVec->begin(), optVec->end(), std::ostream_iterator<int>(std::cout, ", "));

Conclusion

std::optional is a handy, little, but powerful library feature. The next time you try to figure out what the magic value for “nothing” should be, remember std::optional.

Previous Post
Next Post

6 Comments


  1. I’d be really tempted to write that last bit of code with

    if (auto opt = firstEvenNumberIn(text) )
    {
    // use *opt

    Would this work, is it a standard idiom, and are there any pitfalls hiding here?

    Reply

    1. It would work as you expect. I am not sure I like the notion though. It does constrain the visibility of the variable to the scope where it actually is used, but it also crams function call/variable initialization and condition check into one spot, which hinders the flow of reading the code a bit.
      The same goes for many uses of the new if (init; condition) and while (init; condition) syntax as well as for nontrivial for loop headers.

      Reply

      1. I agree with Jonathan. I have already seen this pattern many times. The explicit operator bool() also hints that it has been designed to work in this way. I would say this is a question of convention; until it becomes one, it may look weird and hinder code reading, but in the long run, the scoping advantage is worth it, in my opinion, and we’ll get used to it.

        I would also add “const” to “auto opt”, to make all variables immutable unless otherwise needed.

        Reply

      2. Do we have while (init;condition) defined by the standard or supported by any compiler now?

        Reply

        1. My bad, we got if(init;condition) and switch(init;condition) with C++17. There’s no such thing for while because we have for(init;condition;update) already

          Reply

    2. I’d be tempted to write this as
      if (auto opt = firstEvenNumberIn(text) ; opt)
      {
      // use *opt
      }
      using Selection Initialization. I think it expresses the intent more clearly having first initialized opt and then evaluating the condition.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *