A templated optional
A concrete optional
Since at least the last post our optional is finally (at least somewhat) feature complete and ready for use.
- It serves a specific purpose (which is holding either a value or no value at all) defined by its invariant.
- It has a interfaces which is arguably easy to use correctly and hard to use incorrectly.
- It is regular, which allows for broad use, e.g. in combination with the STL.
- It is const correct.
But out optional is limited the values of type unsigned int
. This is unfortunate: technically – especially in C++ –
there is no reason to limited our self to just one type: we can use
C++’s templates.
A generalized optional
Currently, the value type used in our optional unsinged int
is spread all over our implementation; most importantly in
the declaration of its mValue
but additionally in the constructor and the value()
member function.
class optional_unsigned_int {
public:
// ...
optional_unsigned_int(unsigned int value) {
//...
unsigned int value() const {
//..
private:
//...
unsigned int mValue;
};
In order to make out optional work with any other type, we need to replace all occurrences of unsigned int
with a
placeholder. Let’s call it T
(for “type”). This can be done by simple text replacement; basically every test editor or
IDE can do this.
But this isn’t enough. The compiler will complain, that T
“does not name a type”. We need to tell the compiler, that
T
is just a placeholder for a type. This is done by using the template
keyword in front of the class definition.
This keyword turns a class into a template class.
template<typename T>
class optional_unsigned_int {
public:
// ...
optional_unsigned_int(T value) {
//...
T value() const {
//..
private:
//...
T mValue;
};
In the angle brackets <
and >
the template’s list of parameters is given. If a parameter is mean to denote a type,
either typename
or class
must precede the name. I prefer typename
over class
, because it is a bit more precise,
as T
actually names a type and is not necessarily a class
.
Now our optional is a template, but with a rather silly name! It doesn’t make any sense to have _unsigned_int
as part
of our optionals name, so we can just remove it.
template<typename T>
class optional_unsigned_int {
public:
// ...
optional(T value) {
//...
friend bool operator ==(optional a, optional b) {
//...
T value() const {
//..
private:
//...
T mValue;
};
Please note, that we are using optional
instead of optional<T>
in the definitions of the signatures of the
comparison operators. We can do this, because we are in the definition of optional<T>
. If we had defined these
functions outside of the class definition, we couldn’t do that. Inside of this class definition optional
and
optional<T>
can basically be used interchangeably. I decided to use the former, because it is simpler and less
repetitive.
Now our optional is optional<T>
instead of optional_unsigned_int
. But now, our test won’t compile, because the are
written in terms of optional_unsigned_int
, which is not defined anymore. For the time being, we can fix this by
introducing a typedef
for our tests.
typedef optional<unsigned int> optional_unsigned_int;
Now our tests are working again. Actually, from a testing point of few, we didn’t change anything. optional<unsigned
int>
will actually basically generate the same code as optional_unsigned_int
did. If we use optional<unsigned int>
, the
compiler will replace all occurrences of T
in the class definition with unsigned int
, hence instantiate the
template class. This instantiantion of the template class will behave like the original optional_unsigned_int
did.
Essentially, the compiler will roll back our changes, we did to our implementation in this post.
We can actually (at least for such simple cases without template specializations
) think of template instantiation as
simple replacement. In The Design and Evolution of C++ Bjarne Stroustrup explains,
that before he invented templates, he tried to solve similar problems by using the C preprocessor (which does basically
nothing else then test replacement). He invented templates to overcome some of the weaknesses of this approach. If you
want to dive deeper into this topic, I recommend reading this book to you. It gives great insights in the early history
of C++ and explains really well the reasons, why many things in the language are like they are.
Conclusion
Well, that was easy! Replacing unsigned int
with a T
, adding a template<typename T>
and introducing a better name
was enough to generalize our optional. Actually this post was intended to take a bit of the fear about writing template
classes from you: it is basically as easy as this; at least it can be.
But this generalized optional is far from being optimal. For build-in types like int
, char
or float
is will work
quite well, though. But for other – more complex – types, this is not the case. In the upcoming posts, we will have a
look at the issues, which may arise with this rather naive implementation as soon as we are using it with such more
complex types.