Assign for reset

Recently we we introduced optional::reset() to make any optional empty. To some extent we did this to be consistent with the smart pointers as they also have such reset function. It runs out that there is a mechanism for archiving the same thing as call reset() on a smart pointer and which works for raw pointers, too: we can assign a null pointer (nullptr). It would be quite nice to have something similar for out optional, too!

Something like a nullptr for optional

In order to make clear what we want to archive, let’s have a look at a simple example first! For std::unique_ptr in C++11 we could write something like this:

std::unique_ptr<int> ptr;
ptr = nullptr;

Please note that this only possible in C++11 not only because std::unique_ptr has been introduced in C++11 but also because nullptr has been added to the language in C++11, too.

In C++11 there are two new things with regard to null pointers:

Both are important here. nullptr (the literal) is needed in order to be able to write the code above. The type std::nullptr_t allows the definition of an overload of unique_ptr’s assignment operator for exactly that case. This overload of the assignment operator has the same effect as calling reset().

We need something similar for optional. We can and should not use nullptr though because:

  1. As we are still using C++98 we cannot use nullptr, because it’s not available yet.
  2. In case of a function overloaded for a pointer and a optional this will cause an ambiguity – we will get back to that.

So we need to come up with our own solution for that: we need another type and a constant for this reason. This is what nullopt_t and nullopt. Defining nullopt_t is quite easy: an empty struct will suffice. Given that, we can already implement the new overload for the assignment operator.

struct nullopt_t {};

const nullopt_t nullopt;
//...

template <typename T>
class optional {
 // ...
 public:
 // ...
  optional& operator=(nullopt_t) {
    reset();
    return *this;
  }
 // ...
}

Please note, that the overload – just as the original one – return’s a reference to the optional it self. This enables a notation like a = b = nullopt.

Based on the test cases for reset() we already introduced before we can derive new test cases for this new assignment operator.

TEST_CASE(
    "Assigning nullopt to an optional with a value resets the optional.") {
  int anyValueX = 5;
  optional<int> x(anyValueX);
  REQUIRE(x);
  x = nullopt;
  REQUIRE(not x);
}

TEST_CASE(
    "After assigning a nullopt to an optional without a value has still a value.") {
  optional<int> x;
  REQUIRE(not x);
  x = nullopt;
  REQUIRE(not x);
}

Another constructor

We can now write this code:

optional<int> x;
x = nullopt;

But we cannot write this code:

optional<int> x = nullopt;

The reason for that is, that in this case the assignment operator is not used: we need another constructor for this case.

We can easily derive a suitable test case…

TEST_CASE("An optional initialized from nullopt has no value.") {
  const optional_unsigned_int x = nullopt;
  REQUIRE(!x);
}

… and add the missing constructor.

template <typename T>
class optional {
 // ...
 public:
 // ...
  optional(nullopt_t)
      : mHasValue(false) {}
 // ...
}

Equality

Assignment and construction from nullopt is now possible – just as we can assign a null pointer to any other pointer. But what else can we do with a null pointer? We can use it in order to check whether a pointer stores a null pointer! In order to extend our analogy between pointers and optionals even further it makes sense to have the ability to check whether an optional is empty by comparing in with nullopt. As usual we will write tests covering this behavior first:

TEST_CASE("An optional without a value equals nullopt.") {
  const optional_unsigned_int x;
  REQUIRE(x == nullopt);
  REQUIRE(nullopt == x);
  REQUIRE(!(x != nullopt));
  REQUIRE(!(nullopt != x));
}

TEST_CASE("An optional with a value does not equal nullopt.") {
  unsigned int anyValue = 10;
  const optional_unsigned_int x(anyValue);
  REQUIRE(!(x == nullopt));
  REQUIRE(!(nullopt == x));
  REQUIRE(x != nullopt);
  REQUIRE(nullopt != x);
}

If we try to compile our tests now we will get a compiler error:

In file included from /home/user/optionalcpp/tests/tests.cpp:2:
/home/user/optionalcpp/include/optional.hpp: In instantiation of bool operator==(const optional<T>&, const U&) [with U = nullopt_t; T = unsigned int]:
/home/user/optionalcpp/tests/tests.cpp:18:3:   required from here
/home/user/optionalcpp/include/optional.hpp:170:15: error: no match for operator== (operand types are const unsigned int and const nullopt_t)
  170 |     return *a == b;

The reason for that are the overloads for the == and != operator we had introduced as an optimization for heterogeneous comparisons: the non-optional parameter will happily take anything we pass to – including nullopt. Therefor the respective overload will be chosen which then causes compilation to fail because nullopt can usually not be compared with the value type.

Hence, we need to provide separate overloads for these cases:

template <typename T>
class optional {
 // ...
 public:
 // ...
  friend bool operator==(nullopt_t, const optional& b) {
     return not b.mHasValue;
  }

  friend bool operator==(const optional& a, nullopt_t) {
     return nullopt == a;
  }
 // ...
}

With these two our tests compile successfully and all our tests succeed.

Please note that we didn’t need to provide the respective overloads for !=. The reason for that is that we implemented the heterogeneous overloads of the != operator in terms of the == operator. If we try to compare an optional with a nullopt using the != operator, the compiler will take the heterogeneous overload of it – just as it did it for == above. But in this case the compilation success because here we are delegating to the == operator, which we just had implemented.

The Other Comparisons

As we now have == and != for nullopt it makes sense to also introduce the over comparison operators. We can treat nullopt just like an optional without a value, so that the same rule for the comparison operators apply: an nullopt is always considered to be smaller than an optional with a value. Given that we can add appropriated tests rather easily:

TEST_CASE("An optional without a value equals nullopt.") {
  const optional_unsigned_int x;
  REQUIRE(x == nullopt);
  REQUIRE(x <= nullopt);
  REQUIRE(x >= nullopt);
  REQUIRE(nullopt == x);
  REQUIRE(nullopt <= x);
  REQUIRE(nullopt >= x);
  REQUIRE(!(x != nullopt));
  REQUIRE(!(x < nullopt));
  REQUIRE(!(x > nullopt));
  REQUIRE(!(nullopt != x));
  REQUIRE(!(nullopt < x));
  REQUIRE(!(nullopt > x));
}

TEST_CASE("An optional with a value does not equal nullopt.") {
  unsigned int anyValue = 10;
  const optional_unsigned_int x(anyValue);
  REQUIRE(!(x == nullopt));
  REQUIRE(!(x <= nullopt));
  REQUIRE(!(nullopt == x));
  REQUIRE(!(nullopt >= x));
  REQUIRE(x != nullopt);
  REQUIRE(nullopt != x);
}

TEST_CASE("An optional with a value greater than nullopt.") {
  unsigned int anyValue = 10;
  const optional_unsigned_int x(anyValue);
  REQUIRE(x > nullopt);
  REQUIRE(x >= nullopt);
  REQUIRE(nullopt < x);
  REQUIRE(nullopt <= x);
}

Just as we did before I’ve chosen to extend the equality and inequality test case a bit in order to avoid duplication. Based on that we can now implement the < operator.

template <typename T>
class optional {
 // ...
 public:
 // ...
  friend bool operator<(nullopt_t, const optional& b) {
     return b.mHasValue;
  }

  friend bool operator<(const optional& a, nullopt_t) {
     return false;
  }
 // ...
}

The new tests will compile and succeed now. There are two things noteworthy here:

  • again, it is enough to add overload to the < operator. This is (again) because of the heterogeneous comparisons and the fact that the other comparison operators are implemented in terms of <.
  • We have an interesting asymmetry here: it depends on the order of the operands whether the optional matters or not. If we give it a seconds thought, this makes totally sense: an optional can never the less than an optional without a value. If can be greater or equal to it but never less.

Conclusion

Once again we took the syntax of pointer as a guiding line for extending the design of optional: we introduced nullopt as an null pointer equivalent. We anticipated the introduction of nullptr in C++11, but we could implement nullopt_t and nullopt in C++98 already. We can now write:

  • optional<T> x; x = nullopt,
  • optional<T> x = nullopt,
  • x == nullopt and e.g.
  • x < nullopt

We introduced nullopt as a global variable. For the moment this works quite well, but in general globals may cause issues if used in the context of shared and static libraries. However, we will have a look at this in an upcoming post.