Skip to content

Modern C++ • Optional values

Handling return values from function calls is a common activity in computer programming. However, in many cases 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.

One typical occurrence is when the call to a function cannot be fulfilled for some reason. For instance, when an expected error occurs during the execution of the function. In this case there is the need to signal the outcome to the caller in some way. Consider as an example the following situation: a system that implements an “items count” module, that is a component that counts the number of items observed in a period of time, 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 temperature reading module. It can return both positive and negative values. However, 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, this works as long as max values are guaranteed to never occur and have no meaning in the return values. 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) {
   const auto& [found, rec]{ db.find(id) };
   if (found) {
      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 the query fails and 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 one 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.

Another approach may be to use a pointer Record* (or, better, a smart pointer) as a return type and return a null pointer for the undefined value. 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, or pairs of fields where one field serves as a flag to indicate whether the result is defined or not and the other to hold the actual value. This latter solution could be implemented, for example, with std::pair. But again, it would require writing clumsy code and does not have embedded error checking (one could use the value even when it’s undefined without getting any warning).

The cleanest solution to this problem would be that of creating a specialized “maybe has result” class that clearly conveys the semantic 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:
   const T m_val;
   const 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) {
   const 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 check that prevents 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.

To represent an undefined value with std::optional, simply create a default-constructed one

#include <optional>

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

// function returning an optional int value
std::optional<int> f() {
   // ...
   return std::optional<int>{};
}

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

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'

// function returning an optional int value
std::optional<int> f() {
   // ...
   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 been achieved by using several methods in old C++. Starting with C++17 there is a unified and consistent method 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 a good fit.

Published inModern C++