An C++ 98 Optional: Part I - Groundwork
Where we left C++98
Since we introduced a union in our optional, we had to move from C++98 to C++11 because C++98 does not support unrestricted unions. But since back then we learned, that we can use the placement new operator to construct any object at a given address. This knowledge gives us the opportunity to implement a minimal core optional in C++98.
But before we approach this matter, we should lay the foundation first. There are two things we can do to make the change easier and there is one necessary change unrelated to the actual issue.
Preliminary step I - Less direct access to mValue
It is already pretty much clear, that we won’t be able to access the value optional just like any normal member variable
using mValue
. Therefore we should limit the use of mValue
in our code to the bare minimum. We can use optional’s
dereference operator for that.
With these changes, mValue
is less frequently used in the code, which makes replacing it much easier.
Preliminary step II - Factor out value construction and deconstructions
If we have a closer look at our code, we will see, that T
s copy constructor and its destructor are called in multiple
places. We should factor them out into separate functions so that we only need to change them at one place each instead
of multiple.
Please note, that we also removed mNoValue
from the default constructors initializer list. Initializing the union to
mNoValue
has never been necessary, but it made obvious, that mValue
is not initialized in the default constructor.
As soon as we remove the union, mNoValue
will be gone, too, so we would be required to remove it anyways soon.
Preliminary step III - Remove the explicit
keyword
A second preliminary step is to remove the explicit
key word from the conversion-to-bool
-operator, which is not
supported in C++98. As this keyword is
not necessary anymore, we can remove it for this step back to C++98.
Conclusion
This times, the changes weren’t spectacular or exiting in any way, but these changes were necessary to simplify our transition away from the union as the underlying mechanism for our optional. Now we focus on these parts of the code for the upcoming changes:
PS: Don’t forget the tests!
Actually, we managed to use C++11 features in our tests without noticing it:
- braced initialization,
>>
instead of> >
in templates,- list-initialization for
std::vector
and - local types in template instantiations.
For the sake of completeness the necessary adaptions are listed here as well.
Braced initialization
In C++11 we could used curly brackets ({}
) for calling constructors.
ES.23 of the C++ Core Guidelines advises to use
them (for very good reasons), but in C++98, they are just not available. We must go back to the old fashioned syntax using round brackets
(()
).
This compiles – but the tests are failing. If turns out, that this is a good example of the most vexing parse. Let’s have a look at the whole test.
The test fails, because the first assertion fails: missingDestructorCalls
equals 0 instead of 1.
This implies, that no constructor has been called at this point. After I encountered this issue, I was thinking:
“Well, so if the constructors are not called, let’s check if the optional stores a value!”, so I added a
REQUIRE(x)
. This resulted in a linker error:
This tells ous, that the compiler interprets the definition and initialization of the optional as a function
declaration. CheckedDestructorCalls()
is interpretec as a function pointer. hence x
is interpreted as the
delclaration of a function, which takes such a function pointer and returns an optional. Note that, the linker error
does not mention the “return value” of the aleged function. This is because the linker doesn’t know it. It is not
encoded in the symbol of the function.
The Wikipedia article about the most vexing parse mentions exactly this issue and suppgests multiple fixes for it. We will stick with copy initialization.
This whole issue shows us one reason for prefering {}
over ()
for initialization: avoiding the most vecing parse.
Nicer Template Syntax
In C++98 the token >>
is always interpreted as the
right shift operator – even in cases in which it is
obviously not intended to be a shift operator like template instantiations. Because of the reason, the following code
will not compile although it is perfectly fine C++11 code.
In order to fix this, we need to add one space so that >>
becomes > >
.
List-initialization for std::vector
In C++11 we could initialize a std::vector
– just like a built-in array – using
list initialization.
This features has been introduced with C++11: std::initializer_list
has been added to C++ standard, so that the
respective constructors can now be written. In C++98 we need to add the elements manually to the std::vector
.
Function local types in templates
In C++98 and C++03 it was not allowed to use function local types in templates. This is the reason why we can not continue using function local structs in our tests like this:
Thankfully, this can be easily fixed by moving the definition of this struct out of the test case definition.