An invariant for optional
The issue
Using the constructors we implemented in the last post, we can create valid optionals. But with our current design it is rather easy to invalidate such a valid optional afterwards. Let’s have a look at the following example.
We can easily invalidate a valid optional without a value by setting is_set
to true
; if we do this we encounter the
in the same issue as described in the
last post: we may read from
uninitialized memory.
The main issue here is, that we can modify is_set
without touching value
. This leads us to an important concept of
class design: the class invariant
.
What is a class invariant ?
As already stated in the
last post, there is a connection
between the two members is_set
and value
of our optional: is_set
tells us, whether value
stores a valid value.
Such a connection can usually be described by a logical condition called class invariant.
A class invariant is a logical condition which is
- established by a class’ constructor and
- constantly maintained between calls to public methods.
In our particular case we can phrase optional’s class invariant like this:
An optional’s value may be read if and only if the optional contains a valid value.
Note that we used this condition already in last post’s example to illustrate why an invalid optional is harmful. The class invariant describes the essential properties which a given class has. It eases designing and developing a class, since we can assume that the class invariant holds true before every public member function call, as well as using a class, since it describes the class concisely. We can even say that maintaining its invariant is a class’ main (or even only) concern.
A class’ members are coupled by the class invariant. If a class has an invariant, it usually has high cohesion as well, which is often an indicator for good design. This means that a concise class invariants can lead to a good software design consisting of small and cohesive classes which are loosely coupled. Invariants also help to reason about code. In fact, invariants are often necessary in order to use design by contract (e.g. the Java Modeling Language). Such contracts can then be used for formal verification.
A first very simple solution
The traditional way of maintaining a class invariant in object-oriented programming is to use
encapsulation. In the end we will use this
technique as well, but I want to show you, that there is another possibility: we can just make all our members const
.
Note that we need to initializes value
in every constructor, because the compiler will complain otherwise.
The constructors are making sure, that the class invariant is established correctly (this is basically, what
constructors are good for:
C.40).
This implementation maintains optional’s class invariant, because the invariant is established during construction and can
not be altered afterwards; the above mentioned issue can not occur, because the members can not be modified after
construction.
This solution impresses with simplicity. As this optional implementation is immutable, it can be considered a purely funcation data structure. Such data structures have advantages especially in case of data shared between multiple threads as no synchronisation is needed (which is one of the reasons why functional programming languages such as Erlang are quite popular in distributed computing).
But there are reasons why we should not stick with this implementation.
- C++ is a multi-paradigm language. An optional should not only support functional but also e.g. procedural programming. Therefore assignment to an optional should be allowed.
- The solution above is simple because it is concrete (as opposed to generic) and uses a build-in type for
value
. Soon we will generalizeoptional_unsigned_int
tooptional<T>
. For a non-trivial typeT
, it might be undesirable to construct a value of typeT
for optionals without a value (because constructing aT
might be expansive or may have side effects). We will elaborate this issue in a future post. - An optional should be swappable without copying and
movable (from C++11 on). As this leads to instances of optional being modified, optional must be mutable to allow
these operations. For our current
optional_unsigned_int
this concern doesn’t matter, but for a genericoptional<T>
its important to use these operations for performance reasons, ifT
is hard to copy but easy to move.
A more suitable solution
So well will use encapsulation then. First we will – in order with rule
C.2
of the C++ Core Guidelines – make optional_unsigned_int
a class and we will – in order with rule
C.9 – make its
members private and introduce getter-like member functions.
The names of the member functions has_value()
and value()
are taken from std::optional
. Additionally I renamed the
member variables because I had to rename value
anyways in order to avoid name clashes. Of cause the tests must be
adapted accordingly. It makes not much sense to add new tests though, because we did not implement new behavior of the class.
Note: const correctness is not yet considered. We will introduces const correct member functions in an upcoming post.
Conclusion
Finally optional_unsigned_int
is a class instead of a struct. We made it a class, because
it must maintain its class invariant. We choose to use encapsulation and member functions over immutability to
enable a broader use of optional.
In the upcoming posts we will talk a bit more about some general desirable properties of an optional. After that, we can start to generalize our optional.