std::optional and you

Part one of the “memorysafe C++” series will look at how std::optional can help us to write safer code, that is easier to reason about. Optional is used to safely express a value that might or might not exist. Traditionally this has been solved by using either NULL pointers or dummyvalues/errorcodes. Let’s have a look at an example:

int32_t SomeCalculationThatMightFail(int32_t p1, int32_t p2)
{
if (p1 < p2)
{
return -1;
}
else
{
return p1 - p2;
}
}

Here we use a dummyvalue (-1) to communicate an errorcase. Things have been done that way for a long time and a lot of common APIs use these kind of errorcodes (*coughcough* POSIX *coughcough*). This method has a couple of problems that increase the likelihood of errors:

  • -1 is strictly speaking a valid int32_t, so as a consumer of the method, that has not read the documentation, we might assume this is a “normal” returnvalue.
  • In this very case, we actually would not need to use int32_t as a returnvalue, a uint would be better suited, however we have clipped off half of the range of int32 to have a single error code.
  • The caller of the function is free to not handle -1 and get into trouble. This is especially true for cases, where the value we return is actually a pointer and we return NULL/nullptr in the error case. Yes, this is usually punished with at least a warning if you lint your code, but it would actually be better to force the programmer into dealing with the error case.

Refactor to use optional

The change is simple:

std::optional<int32_t> SomeCalculationThatMightFail(int32_t p1, int32_t p2)
 {
     if (p1 < p2)
     {
         return {};
     }
     else
     {
         return p1 - p2;
     }
 }

Now, what have we gained? Using optional we can now express the fact, that we have no result whatsoever in a clean way, if we look at the callsite:

void TestOptional()
 {
     if (auto result = SomeCalculationThatMightFail(10, 20))
     {
         std::cout << "returned " << *result;
     }
     else
     {
         std::cout << "No value returned";
     }
 }

The caller of our function no longer can just use the value, as it is wrapped in an optional. We also have clearly seperated the error case from the “regular” case with respect to the return value. The good thing here is, that the assignment will yield false, if SomeCalculationTheMightFail returns an empty object. This gently nudges the caller towards using the returnvalue the way it is intended to be used, as the difference between just getting the value and getting the value while also checking for validity is negligible. Yes, the programmer can still go ahead and just *result, which is actually worse than the previous variant, as we now are guaranteed to have undefined behavior, however I’d still consider using optional to be very worthwhile as it encourages “doing it the right way”.

std::optional ist available with C++ compilers supporting C++17.

Image Credit: Christopher Burns

Leave a Reply

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