Immutability

Since at least functional programming gained more and more popularity among programmers outside the academic sphere, immutable data is considered a good thing. C++ supports the immutability of variables or objects in form of the const key word since its early days (see The Design and Evolution of C++ for reference): if we declare a variable with this keyword, we may not alter it’s value after initialization.

// initialize a constant
const int x = 5;
// modification is forbidden by the compiler
x = 6;

Making as many variables and objects immutable immutable is considered a best practice in the C++ community:

A few of the most frequently mentioned reasons for using const as much as possible are:

  • Ease of reasoning about a piece of code: we don’t need to keep track of state changes of variables if there aren’t any.
  • The compiler enforces constants to be unchanged: this may turn some programming errors into compiler errors.
  • Immutability can be very helpful in multithreaded contexts.
  • Striving for constness helps to avoid the “Initialize Then Modify” Antipattern.

These arguments should be conclusive enough to convince us that immutability is a good thing and that therefore our optional should support immutability. We shoudn’t make the members of our optional constant though! If we would do that, we would use /assignability/. As soon as one member of a class/struct is const, assignment on this object is disallowed, because this would require to reassign a constant, which is impossible. As our optional should keep being a regular type, we should refrain from this idea (C.12 also advises us to not to make data members const).

But the least we can do is making sure that our optional may be declared and used as a constant.

Constant optionals in the tests

So the first thing, we should do, is to use constant optionals in our tests; for example in these two cases:

TEST_CASE("A default constructed value is not set.") {
  const optional_unsigned_int x;
  REQUIRE(x.has_value() == false);
}

TEST_CASE("An optional constructed with a value is set and stores the value.") {
  unsigned int anyValue = 10;
  const optional_unsigned_int x(anyValue);
  REQUIRE(x.has_value() == true);
  REQUIRE(x.value() == anyValue);
}

But if we try to build our tests now, we will get a compiler error: passing ‘const optional_unsigned_int’ as ‘this’ argument discards qualifiers.

Member functions and constants

In order to understand this, we should take a step back and have a look on a reduced version of our optional.

class optional_unsigned_int {
 public:
  optional_unsigned_int()
      : mHasValue(false) {}

  optional_unsigned_int(unsigned int value)
      : mHasValue(true)
      , mValue(value) {}

  bool has_value() {
    return mHasValue;
  }

  unsigned int value() {
    return mValue;
  }

 private:
  bool mHasValue;
  unsigned int mValue;
};

For us as a reader, it is pretty clear, that none of the member functions has_value() and value() alters the classes members, so for us there may be no particular reason, why it shouldn’t be possible to call these functions on a constant optional.

The reason is, that the compiler basically treats theses member function as if the we something like this:

bool has_value(optional_unsigned_int * this) {
  return this->mHasValue;
}

unsigned int value(optional_unsigned_int * this) {
  return this->mValue;
}

Every non-static member function takes a pointer to the object, the function is called on, as the this pointer. If these member functions are declared as we did above, the this pointer points to a non-const/mutable object. But we can not pass an object which has been declared to be const to a function, which takes a pointer to a non-const object. This is, because these function may alter the object via this pointer, which is not allowed for constants.

optional_unsigned_int mutuable_x;
unsigned int x1 = value(x); // compiles

const optional_unsigned_int mutuable_x;
unsigned int x1 = value(x); // error

With this knowledge, the compiler error we encountered above starts to make sense. We actually passed a const optional_unsigned_int as this which is – in this case – a pointer to a non-const optional_unsigned_int and this discards the const qualifier, which isn’t allowed.

So what can we do about this issue? We need to make the this pointer a pointer to a constant. This is done by adding const to the function definitions.

class optional_unsigned_int {
 // ...
  bool has_value() const {
    return mHasValue;
  }

  unsigned int value() const {
    return mValue;
  }

 // ...
  unsigned int mValue;
};

This const applies to the this pointer implicitly passed to the member function: optional_unsigned_int * this becomes to const optional_unsigned_int * this. With this addition, we can now call these two member functions on a constant optional, we implies that out tests are compiling again without any errors now.

With this addition, out optional also complies to Con.2 of the C++ Core Guidelines: “by default, make member functions const”. We also understand now, why this rule makes sense: it enables the said member functions to be called on const objects.

Conclusion

Our optional is now pretty much usable: it protects it’s invariant, is regular and is const correct. As it is, it would be pretty much ready for everyday use. Now we can finally start to generalize it in order to get closer to the actual std::optional implementation.