Skip to content

Modern C++ • Optional values

One of the most common dilemmas when designing interfaces is how to handle the values returned by functions and/or methods in cases where it cannot be guaranteed that the returned value is reliable or even present at all. In such situations, the need arises for additional information to signal that the expected value is not defined.

A typical occurrence is when the call to a function cannot be fulfilled for some reason. For instance, when an error that may occur during the execution of the function happens. In this case there is the need to signal the outcome to the caller in some way. Consider as an example a system that implements an “object count” module, that is a component that counts the number of objects observed in a period of time, such as in a surveillance system (people count), quality assurance system (product count), etc.

A hypothetical people_count() function returns the number of people detected by an AI-powered camera connected to a network as an integral number. But what happens if, for example, the camera has lost connection to the network, or if the CV algorithm can’t reliably perform the detection? Raising exceptions is not an advisable practice as those are not “exceptional” situations but ones that should be expected, especially if the system operates in an unreliable environment.

The common solution to this problem is the “error code” method, consisting in returning a negative value that can be encoded within the return type itself (an int in our example). Something on the following lines

constexpr auto NO_DETECTION{-1};
 
int get_people_count() {
   if(camera.ready() && camera.has_detected()) {
      return camera.object_count();
   } else {
      return NO_DETECTION;
   }
}
...
auto people_count{ get_people_count() };
 
if (people_count != NO_DETECTION) {
   // Do something
} else {
   // Do something else
}

This works as long as the return type allows conveying extra information together with valid results. In this case, negative values have no meaning so we can use them for our purpose.

However, this is not always possible. In many cases the return value may be a real number taking both negative and positive values. In such situations the solution may be that of using values that would never occur among all possible valid results. For example, consider a software module that controls a sensor to read ambient temperatures. Both positive and negative values are valid readings. Nonetheless, we may assume that very large temperatures will never occur and write the following code

constexpr auto NO_READING{ std::numeric_limits<int>::max() };
 
int get_temperature() {
   if(sensor.ready()){
      return sensor.read();
   } else {
      return NO_READING;
   }
}
...
auto t{ get_temperature() };
 
if (t != NO_READING) {
   // Do something
} else {
   // Do something else
}

Again, as with the previous example, this works as long as max values are guaranteed to never occur and have no meaning in the return values. Methods like these that try to encode errors using special values within the same domain of the return type are also known as the out-of-band value methods.

An alternative solution may be that of using a global variable that is set whenever the undefined state occurs. This would solve the issue of choosing a special indicator in the range of the return values, but we all know that keeping global state consistent is difficult and discouraged.

Another case is when the return value is an object. Signalling the absence of a valid return value in such situation is a bit more complicated. Consider as an example a module that queries the database for some kind of record

struct Record {
   std::uint32_t id;
   // ...
};
 
Record get_record(const std::uint32_t id) {
   auto [found, rec]{ db.find(id) };
   if (found) {
      // Process record...
      return rec;
   } else {
      // what to return ?
   }
}

The database query method returns a result flag and a record object if the operation is successful. However, if no record is found the function should signal this event in some way. There are a few solutions that could be implemented. For example, instead of a record structure we may use a std::vector<Record> and return an empty vector to indicate that there is no record. But we’d be using something that’s not meant to be used for that purpose. Not the best solution for sure.

An alternative method may be adding a specific “error field” to the record and use it to check the result of operations involving the record. This solution “just works” but it too has a few serious issues, including questionable design practices (error handling into the domain’s data entities), inability to modify the Record class because it belongs to a 3rd party library, etc.

Another approach may be to use a pointer Record* (or, better, a smart pointer) as a return type and return a null pointer when the value is undefined. Again, while this works and is a common solution in old C++ code, a null pointer may as well be the result of other failures (e.g. memory allocation failures) and cause ambiguities.

Other solutions may involve using special “empty/null objects” (i.e. zero-initialized structures), but they still require implementing extra code and must guarantee the uniqueness of their meaning to avoid conflicts. Returning pairs of fields where one serves as a flag and the other to hold the actual value – like in the above example – does not provide embedded error checking and requires writing it manually each time. And forgetting to do so may cause using an undefined value without getting any warning.

The cleanest solution to this problem would be creating a specialized “maybe has result” class that clearly conveys the semantics of its purpose. A class that can be used in all those contexts where getting a result is optional and not guaranteed. It would encapsulate both the result value and the information needed to unambiguously signal when the value is undefined, along with methods that allow querying and getting the value. Such a class may be defined as follows

template <typename T>
class maybe {
 public:
 
   maybe(const maybe&) = default;
   maybe(maybe&&) = default;
   maybe& operator=(const maybe&) = default;
   maybe& operator=(maybe&&) = default;
 
   maybe()
      : m_val{}
      , m_valid{false} {
   }
 
   maybe(const T& val)
      : m_val{val},
      : m_valid{true} {
   }
 
   T get_value() const {
      if (m_valid) {
         return m_val;
      } else {
         throw std::runtime_error("Undefined value");
      }
   }
 
   bool has_value() const noexcept {
      return m_valid;
   }
 
 private:
 
   T m_val;
   bool m_valid;
};

This class can then be used to wrap the actual result into a stateful component. It would allow us to solve the above example problems as follows

maybe<Record> get_record(const std::uint32_t id) {
   auto [found, rec]{ db.find(id) };
   if (found) {
      return maybe<Record>{rec};
   } else {
      return maybe<Record>{};
   }
}
...
auto result{ get_record(101) };
 
if (result.has_value()) {
   auto record{ result.get_value() };
   // Do something with the record
} else {
   // Do something else if there is no record
}

This solution is generic, more object-oriented and provides some checks that prevent accessing an undefined value by rising an exception. It also clearly communicates the intent of its scope: representing a result that may or may not be there.

The modern C++ solution: std::optional

In modern C++ there is no need to come up with self-made solutions and workarounds. It turns out that C++17 has introduced a new class template std::optional that does exactly the job (and much better) of the maybe class presented above. This new type models the case of some object potentially (optionally) containing a value. Other languages use this concept too, such as Haskell’s Maybe type, (the choice of the class name was not casual). Both std::optional and boost::optional (from which the former is derived) are based on the ideas exposed in there.

#include <optional>
 
std::optional<int> opt_int{};
std::cout << std::boolalpha 
          << opt_int.has_value() << std::endl; // prints 'false'
 
std::optional<int> f() {
   //...
   if(result) {
      return 10;
   } else {
      return std::optional<int>{};  // No value
   }
}

Assigning a value will make the optional defined and holding that value

std::optional<int> opt_int{5};
std::cout << std::boolalpha 
          << opt_int.has_value() << std::endl;  // prints 'true'
std::cout << opt_int.value() << std::endl;      // prints '5'

If the optional is undefined, then trying to get its value should be considered an error. This is, in fact, enforced by throwing an exception. The following code will throw if an attempt is made to get the undefined value

std::optional<int> opt_int{};
auto val{ opt_int.value() };  // throws std::bad_optional_access

Thestd::optional class also provides “unchecked” access to the wrapped value that may be more efficient but potentially dangerous causing undefined behavior since no checks are done for the validity of the value

std::optional<int> opt_int{10};
std::optional<int> opt_no{};

std::cout << *opt_int << std::endl;   // OK
std::cout << *opt_no << std::endl;    // UB

There is also a special value that can be used to create an optional with undefined value: std::nullopt. It more clearly conveys the message that the optional does not contain any value.

std::optional<int> opt_int{std::nullopt};
std::cout << std::boolalpha 
          << opt_int.has_value() << std::endl;  // prints 'false'
 
std::optional<int> f() {
   //...
   if(result) {
      return 10;
   } else {
      return std::nullopt;
   }
}

See the C++ reference for a comprehensive list of all the functionality that it offers.

Conclusion

Implementing the concept of optional values has traditionally been achieved using several methods in legacy C++ code. However, all of them have quite a few drawbacks and should be avoided in modern C++. Starting with C++17 there is a better and more consistent solution that is both efficient and safe (*): the std::optional type. So, every time there is the need to model something with the semantic “value or nothing” and (important!) it is not an error if it’s nothing, std::optional is what you’re looking for.

(*) Mind the unchecked access

Published inModern C++