Less Than Comparison for Optional

In order to make our optional regular, we need to provide an implementation of the Less-Than operator <. For two optionals with a value, it makes sense to employ their < operator. Also we already defined that two empty optionals are considered to be equal. Therefore we only need to decide, how an optional with a value shall compare to an optional without a value. std::optional considers an optional without a value to be less than an optional without a value. So our rules for the < operator are:

Given two instances a and b of an optional, a is less than b if and only

  • if a has no value and b has a value.
  • if both have values and the value of a is less than the value of b.

We can now derive test cases for these two cases.

TEST_CASE("An optional without a value is less then an optional with a value.") {
  optional_unsigned_int x;
  unsigned int anyValue = 10;
  optional_unsigned_int y(anyValue);
  REQUIRE(x < y);
}

TEST_CASE("An optional 'x' is less than an optional 'y' if the value of 'x' is smaller than the value of 'y'.") {
  unsigned int anyValueX = 5;
  unsigned int anyValueY = 6;
  REQUIRE(anyValueX < anyValueY);
  optional_unsigned_int x(anyValueX);
  optional_unsigned_int y(anyValueY);
  REQUIRE(x < y);
}

We need to provide an implementation for the < operator now. Just as it was the case for the == operator, we could implement it as a member function, but we will implement it as a friend function for similar reasons like we did it for the == operator.

class optional_unsigned_int {
 public:
 // ...
 friend bool operator <(optional_unsigned_int a, optional_unsigned_int b) {
   return true;
 }
};

This implementation will – similarly as out first implementation of the == operator – obviously make our tests pass but it is obviously wrong. Again, we need to implement a few tests for the negative cases. We could obviously write them down one by one, but we can also reuse our existing test cases: if two optionals are equal, one can never be less than the other. So we will just extend the existing test cases with checks for the < operator.

TEST_CASE("Two optionals without a value are equal.") {
  optional_unsigned_int x;
  optional_unsigned_int y;
  REQUIRE(x == y);
  REQUIRE(y == x);
  REQUIRE(!(x != y));
  REQUIRE(!(y != x));
  REQUIRE(!(x < y));
  REQUIRE(!(y < x));
}

TEST_CASE("Two optionals with equal values are equal.") {
  unsigned int anyValue = 5;
  optional_unsigned_int x(anyValue);
  optional_unsigned_int y(anyValue);
  REQUIRE(x == y);
  REQUIRE(y == x);
  REQUIRE(!(x != y));
  REQUIRE(!(y != x));
  REQUIRE(!(x < y));
  REQUIRE(!(y < x));
}

Now our tests are failing, so we need to provide a proper implementation.

class optional_unsigned_int {
 public:
 // ...
 friend bool operator <(optional_unsigned_int a, optional_unsigned_int b) {
    if (a.mHasValue && b.mHasValue) {
        return a.mValue < b.mValue;
    }
    return !a.mHasValue && b.mHasValue;
  }
};

All our tests are now passing again.

If we now have a second look at our test cases, we may see that there is some redundancy: the negative test cases for the == operator and the positive test cases for the < operator can be merged. It seems reasonable to add the checks for the == operator to the tests cases for the < operator.

TEST_CASE("An optional without a value is less then an optional with a value (they are unequal).") {
  optional_unsigned_int x;
  unsigned int anyValue = 10;
  optional_unsigned_int y(anyValue);
  REQUIRE(x < y);
  REQUIRE(!(x == y));
  REQUIRE(!(y == x));
  REQUIRE(x != y);
  REQUIRE(y != x);
}

TEST_CASE("An optional 'x' is less than an optional 'y' if the value of 'x' is smaller than the value of 'y' (they are unequal).") {
  unsigned int anyValueX = 5;
  unsigned int anyValueY = 6;
  REQUIRE(anyValueX < anyValueY);
  optional_unsigned_int x(anyValueX);
  optional_unsigned_int y(anyValueY);
  REQUIRE(x < y);
  REQUIRE(!(x == y));
  REQUIRE(!(y == x));
  REQUIRE(x != y);
  REQUIRE(y != x);
}

The Other Comparison Operators

==, != and < are not the only comparison operators in C++, there are also <=, > and >=. Although they are not required for a regular type, it makes still sense to implement them, as users will usually assume, that all of them are available if one of them is available. Thankfully, we can implement all of them in terms of of the < operator and also reuse our existing test cases.

TEST_CASE("Two optionals without a value are equal.") {
  optional_unsigned_int x;
  optional_unsigned_int y;
  REQUIRE(x == y);
  REQUIRE(y == x);
  REQUIRE(x >= y);
  REQUIRE(y >= x);
  REQUIRE(x <= y);
  REQUIRE(y <= x);
  REQUIRE(!(x != y));
  REQUIRE(!(y != x));
  REQUIRE(!(x < y));
  REQUIRE(!(y < x));
  REQUIRE(!(x > y));
  REQUIRE(!(y > x));
}

TEST_CASE("Two optionals with equal values are equal.") {
  unsigned int anyValue = 5;
  optional_unsigned_int x(anyValue);
  optional_unsigned_int y(anyValue);
  REQUIRE(x == y);
  REQUIRE(y == x);
  REQUIRE(x <= y);
  REQUIRE(y <= x);
  REQUIRE(x >= y);
  REQUIRE(y >= x);
  REQUIRE(!(x != y));
  REQUIRE(!(y != x));
  REQUIRE(!(x < y));
  REQUIRE(!(y < x));
  REQUIRE(!(x > y));
  REQUIRE(!(y > x));
}

TEST_CASE("An optional with a value and an unsigned value with the same value are equal.") {
  unsigned int anyValue = 1;
  optional_unsigned_int x(anyValue);
  REQUIRE(x == anyValue);
  REQUIRE(anyValue == x);
  REQUIRE(x >= anyValue);
  REQUIRE(anyValue >= x);
  REQUIRE(x <= anyValue);
  REQUIRE(anyValue <= x);
  REQUIRE(!(x != anyValue));
  REQUIRE(!(anyValue != x));
  REQUIRE(!(x < anyValue));
  REQUIRE(!(anyValue < x));
  REQUIRE(!(x > anyValue));
  REQUIRE(!(anyValue > x));
}

TEST_CASE("An optional without a value is less then an optional with a value (they are unequal).") {
  optional_unsigned_int x;
  unsigned int anyValue = 10;
  optional_unsigned_int y(anyValue);
  REQUIRE(x < y);
  REQUIRE(y > x);
  REQUIRE(x <= y);
  REQUIRE(y >= x);
  REQUIRE(!(x == y));
  REQUIRE(!(y == x));
  REQUIRE(x != y);
  REQUIRE(y != x);
}

TEST_CASE("An optional 'x' is less than an optional 'y' if the value of 'x' is smaller than the value of 'y' (they are unequal).") {
  unsigned int anyValueX = 5;
  unsigned int anyValueY = 6;
  REQUIRE(anyValueX < anyValueY);
  optional_unsigned_int x(anyValueX);
  optional_unsigned_int y(anyValueY);
  REQUIRE(x < y);
  REQUIRE(y > x);
  REQUIRE(x <= y);
  REQUIRE(y >= x);
  REQUIRE(!(x == y));
  REQUIRE(!(y == x));
  REQUIRE(x != y);
  REQUIRE(y != x);
}

Based on these tests, we can now implement the missing operators <=, > and >= by delegating – directly or indirectly – to <.

class optional_unsigned_int {
 public:
 // ...
 friend bool operator >(optional_unsigned_int a, optional_unsigned_int b) {
     return b < a;
 }

 friend bool operator >=(optional_unsigned_int a, optional_unsigned_int b) {
    return !(a < b);
 }

 friend bool operator <=(optional_unsigned_int a, optional_unsigned_int b) {
    return !(a > b);
 }
};

Conclusion

All comparison operators are now implemented for our optional. Especially the < operator finally permits use cases like sorting a vector of optionals

void sort_them(std::vector<optional_unsigned_int>& opts) {
  std::sort(opts.begin(), opts.end());
}

or using an optional as the key of a std::map.

std::map<optional_unsigned_int, std::string> m;
m[optional_unsigned_int()] = "no value";

In an upcoming post, we will have a look at the remaining operations in order to make sure that our optional is regular.