Recap Regularity

A few posts ago we set the goal to make our optional. Until now, we implemented almost all of them.

Operation C++ Syntax State of Implementation
default constructor T a; implemented in “Constructors for valid optionals”
copy constructor T a = b; not implemented yet
assignment a = b; not implemented yet
equality a == b implemented in “Equality and Inequality”
inequality a != b implemented in “Equality and Inequality”
ordering a < b implemented in “Ordering Optionals”

Only copy construction and assignment are left. As both operations are basically creating a copy of an optional, we will cover them both at once.

One may come up with a definition of copy, that involves copying all members one by one, but as we already defined equality, it is easier – I suppose – to define copy in terms of equality:

An optional and it’s copy are equal.

Yes, it’s as simple as that. For our current optional implementation, this is completely sufficient.

Deriving Tests

We can now derive test cases from this definition. We have to cover two different operations: copy construction and assignment. Additionally we should cover optionals with and without a value for both operations. This makes a total of four tests.

TEST_CASE("An optional and its copy are equal.") {
  SECTION("copy constructor without a value") {
    optional_unsigned_int x;
    optional_unsigned_int y(x);
    REQUIRE(x == y);
  }

  SECTION("copy constructor with a value") {
    unsigned int anyValueX = 5;
    optional_unsigned_int x(anyValueX);
    optional_unsigned_int y(x);
    REQUIRE(x == y);
  }

  SECTION("copy assignment without a value") {
    optional_unsigned_int x;
    optional_unsigned_int y;
    y = x;
    REQUIRE(x == y);
  }

  SECTION("copy assignment with a value") {
    unsigned int anyValueX = 5;
    optional_unsigned_int x(anyValueX);
    optional_unsigned_int y;
    y = x;
    REQUIRE(x == y);
  }
}

Please note that I have chosen to use a single TEST_CASE and multiple SECTIONs. Each of this SECIONs introduces it’s own scope, so we can reuse the variable names. We could also have created four different test cases. Before we get to the reason, why I didn’t, let’s have a look at a neat little feature of catch: the --list-test-names-only flag. If you pass it to a test executable, it will print out a list of all test names. If we do so with our test executable ./test_optionalcpp --list-test-names-only, we get this output.

A default constructed value is not set.
An optional constructed with a value is set and stores the value.
Two optionals without a value are equal.
Two optionals with equal values are equal.
An optional with a value and an unsigned value with the same value are equal.
An optional without a value is less then an optional with a value (they are unequal).
An optional 'x' is less than an optional 'y' if the value of 'x' is smaller than the value of 'y' (they are unequal).
An optional and its copy are equal.

This now reads more like a description of our optional’s features. We explicitly have chosen to name our tests in a way, that the become “they are executable specification” . Using this flag, we can now actually print out the specification of our optional.

Getting back to the question, why I have chosen to add one test case with four sections instead of four test cases, you might have already a rough idea about my reason. Above we came up with one simple rule, how copy is supposed to work. This rule should be sufficient – more rules are not needed. Using the “one test cases, four sections”-approach, we have added one more line to our list of test names. With the “four test cases” approach we would have added four test names. This would have somewhat polluted our list of test names – and therefore our specification. The specification, as it is now, is pretty concise, which is nice.

If we now compile our tests and run them, we will see that they all pass!. We didn’t do anything the make the pass. How can that be?

Trivial Types

Our optional, as it is right now, has tow members: an boolen (bool) and a integer (unsigned int). Both of them are scalar types build into the language and therefore they are trivial copyable. In general this means, that both of them can be copied byte by byte without any issues (please note, you can not do that with types like e.g. std::string which contain (owning) pointers or references). We can put it this way: trivial copyable types can be copied without using a copy constructor or assignment. As both members of our optional are trivial copyable and we haven’t defined neither a copy constructor, a copy assignment operator, nor a destructor, our optional is trivial copyable as well. In this case, the compiler will generate the needed copy constructor and the assignment operator for us: we don’t need to define them.

Actually we shouldn’t define them in this case because of basically two reasons:

  • It is in general advisable to define none of these operations if not needed. This is also known as the ‘rule of zero’ or C.20 of the C++ Core Guidelines.
  • As soon as we define one of these operations on our own, the class type becomes immediately non-trivial (see this tiny example in compiler explorer). It would be a shame, if a type which could be trivial is not, because of a superfluous copy constructor. There are places, where basically only trivial types can used, e.g. std::atomic (if no template specialization exists, which is usually not the case for user defined types).

If you want to dive deeper in to the topic of trivial types in C++, you should have a look at Jason Turners Talk “Great C++ is_trivial”.

So, we are finished now: we have made our optional regular!

Conclusion

In this post we got to know trivial copyable types and the ‘rule of zero’. We now know for sure, that our optional is regular now. Our optional is pretty much usable now, beside one remaining issue, which we will tackle in the upcoming post.