The return of assignment
Getting back to regular
Since we had to implement our own destructor and copy constructor (if we want to be able to assign to optionals with a non trivial value type), we have to assume, that we need to implement our own copy assignment operator as well. After that our optional we be regular again.
As always, our first step must be to add failing tests. As our existing test cases regarding copy assignment are residing in the “An optional and its copy are equal.” section of our test suite, we will put these new test cases in there as well.
These tests won’t – of cause – compile: the compiler couldn’t synthesis a
copy constructor for std::string
; there is no reason
to believe, that it could do it for the copy assignment operator – actually it can’t for the same reason: the union
.
As soon as we add any syntactically correct implementation of a copy assignment operator, this tests will compile again.
Note, that this signature of an assignment operator is quite unusual: we’ll get back to that in the upcoming post. With this implementation, the tests are compiling again, but are failing now, but it is a good starting point for the actual implementation.
A first naive approach.
A first naive approach could be to take the copy constructor as an example, only we would only need to make a few adaptions:
- As the assignment operator is not a constructor, it has no initializer list. Therefore we need to assign
other.mHasValue
tomHasValue
. - We should assign
other.mValue
tomValue
isother.mHasValue
is true.
Such an implementation would look like this.
This implementation will compile, but it will raise a SEGFAULT for the “copy assignment with a value (non trivial)”
case. The reason for this is, that the assignment operator is basically a member function, an mValue
is in this case
no valid object. The situation is this:
- If (like in our test case) the target optional of the assignment has now value.
- This means, that
mValue
does not store any valid value. This means, that we can not call any member functions on this object. - The assignment operator is a member function. Actually, we can transform an expression like
a = b;
equivalently toa.operator=(b)
. - This implies, that you can not assign to an invalid object.
- But this is the case in the “copy assignment with a value (non trivial)” test case.
From this issue, we should draw these two conclusions:
- The assignment to an optional without a value must be treated differently.
- We need more test cases.
A few more test cases
Obviously, we need to test these four cases:
Target | Source | Test Case Name |
---|---|---|
optional without a value | optional without a value | “copy assign optional without a value to an optional without a value” |
optional without value | optional with a value | “copy assign optional with a value to an optional without a value” |
optional with a value | optional without value | “copy assign optional without a value to an optional with a value” |
optional with a value | optional with a value | “copy assign optional with a value to an optional with a value” |
The implementations could look like this:
Note, that, if we exclude the test case, of which we know, that it will crash, all other tests are passing now. This gives us a strong indication, that our first naive approach was not that bad after all. We can build upon it.
A better solution
With this version, all our test cases will pass. Please note, that only two cases which we listed above, are covered explicitly:
- The “copy assign optional with a value to an optional with a value” case is covered by the first branch of the if-else
statement. In this case, we can safely rely on
T
’s assignment operator to work properly. - The “copy assign optional with a value to an optional without a value” case is covered by the second branch. In this
case, we need to construct a copy of
other
’s value instead of assigning it to an existing value. - In the “copy assign optional without a value to an optional without a value” there is no value involved, so
mValue
can be ignored. - The “copy assign optional without a value to an optional with a value” case is not covered at all. This should raise suspicion, because the lifetime of the value of the target optional ends with this assignment. Usually at the end of the lifetime of an object, it’s destructor must be called. In our test case, this is quite important because a non-empty vector is used, which uses heap allocated memory. This memory is usually freed within the destructor, but if the destructor is not executed, this memory leaks.
So, our current implementation has a potential memory leak. As memory leaks don’t cause crashed (usually) we were not able to observe it yet. Only if we would perform lot’s of assignments, we would observe, that the memory consumption of the program would increase (it is actually a quite common way of detection memory leaks in larger projects).
Diagnosing memory leaks using valgrind
But there are tools, which can help us to diagnose memory leaks. One of the is valgrind. Please note, that valgrind is Linux tools. For Windows there is Visual Studio’s Deleaker which does similar things.
If we execute out test executable like this, we get an output like this.
Valgrind provides us quite some information here:
- As part of the
HEAP SUMMARY
valgrind tells us the number of allocations (“allocs”) and deallocations (“deallocations). In our case, we have 1389 allocations vs 1388 deallocations, so there is one deallocation missing. This is the proof, that we actually leaks memory. - After that, we see the stack trace of the point in the code, at which
the leaks memory has been allocated. We can see here, that the leaked memory has bee allocated in the optionals copy
constructor (in
optional.hpp
in line 13). Also we can see the line in thetests.cpp
where it happened: 168, the construction ofy
. - Finally there is the
LEAK SUMMAR
, which tells us, that we leaked 12 bytes.
Now, we don’t only know, that there is a memory leak in our tests; we also can now prove our fix.
A better solution without a memory leak
So what do we need to do? We need to destroy the value stored in the target object by calling it’s destructor (if we assign an optional without a value to it).
The tests are still passing, but even, if we run the tests with valgrind, we get an output like this:
And we see: the numbers of allocs
and frees
are matching now! valgrind even tells us explicitly, that there are not
leaks anymore.
Please note, that this implementation circumvents an typical issue of assignment operators: self assignment (see also C.62. There are only two cases:
- the optional has no value: in this case,
mHasValue
is assigned to itself, which is fine, since it is initialized properly. - the optional has a value: in the case,
mHasValue
is assigned to itself, too. Additionally,mValue
is assigned to itself. As we are usingT
’s copy assignment operator here, we can delegate the responsibility to cope with the self-assignment.T
’s copy assignment operator has to make sure, that this case is handled properly now.
Conclusion
In this post, we managed to implement a new assignment operator for our optional. It was a bit harder than implementing
the copy constructor, but this is kind of expected. Especially if one want’s to provide a
strong exception safety for a calls, things may get
complicated. This lead to the wide adoption of the
Copy-Swap idiom. We basically ignored this issue.
Actually, we rely on the exception safety guarantees of T
, because we delegate this responsibility to T
’s
assignment operator. This is actually exactly the same behavior of
std::optional.
Note, that our optional is now regular again. But we are still not done with the assignment operator: there is detailed left open, which we should cover: in the upcoming post.