A null pointer for optional
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:
- the type std::nullptr_t and
- the literal nullptr of type
std::nullptr_t
.
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:
- As we are still using C++98 we cannot use
nullptr
, because it’s not available yet. - 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.