Const Correctness

Writing const correct code is about more than using const in a few places and letting the compiler figure out if it makes sense.

There are two components about using the keyword const in C++ code: A syntactic component and a semantic component.

Syntactic const

The syntactic component is what the compiler figures out at compile time. It does a pretty good job at this: If we declare a variable of a builtin type, e.g. int, as const, then the compiler won’t let us modify it:

int const cantModify = 42;
cantModify = 33; //ERROR

The error message will tell us that we try to assign to a “read-only variable” (GCC) or to a variable “with const-qualified type” (Clang). The same will happen if we have a const instance of a struct or class and directly try to alter a data member:

struct SomeData {
  int i;
  double d;
};

SomeData const data {42, 1.61};
data.i = 55; //ERROR

Methods

Of course, the compiler doesn’t stop here. If we have a method on a class, the compiler by default assumes that it may alter the object on which we call it. We can not call those methods on const objects. Instead, we have to explicitly declare methods const to be able to call them on const objects.

class SomeClass {
public:
  void maybeModify();
  void dontModify() const;
};

SomeClass const someObject{};
someObject.dontModify(); //OK
someObject.maybeModify(); //ERROR

We may get slightly different error messages here, e.g. “passing ‘const SomeClass’ as ‘this’ argument discards qualifiers” for GCC and “member function ‘maybeModify’ not viable: ‘this’ argument has type ‘const SomeClass’, but function is not marked const” for Clang.

The compiler goes even further. When we implement a const method, it checks that we really don’t modify the object. Modifying member data in a const method will cause an error:

class SomeClass {
  int i;
public:
  void dontModify() const {
    i = 47; //ERROR
  }
};

This of course only is done for non-static data members, as static members are not part of the object and therefore can be altered without altering the object.

Limits of syntactic const

Syntactic const is limited in some ways. For example, if we have a const pointer, the pointer itself may not be altered, i.e. where it points to. However, the pointee, i.e. the object it points to, may be altered.

int i = 0; 
int j = 1;
int *const pi = &i;
*pi = 33; //OK - i is now 33
pi = &j; //ERROR - pi is const

This limit of course is also applies for smart pointers and other similar classes.

Semantic const

We can take this example further. Imagine a pointer which is member of a class. In a const method we can not alter the pointer, but we can alter the pointee, as explained above. Now what if the pointee is semantically part of our object?

class Car {
  unique_ptr<Engine> engine;
public:
  void lock() const {
    engine->modify(); //whoops
  }
}

We have to be careful not to accidentally modify objects should be semantically constant but are not syntactically const. This becomes even more apparent if we give back handles to parts.

Engine const& Car::getEngine() const {
  return *engine;
}

The first const here is important, otherwise we would allow user of our class to modify parts of constant objects, which is not a good idea. You can observe this behavior in standard container classes, e.g. for a  vector&lt;T&gt; const tVec, the access operator tVec[0] returns a T const&amp;, although internally the vector only has a pointer to its data.

Not const enough

These examples are rather straight forward. But what if the pointer does not refer to a semantic part of out object but rather to another part of a common larger structure? Consider a binary tree, where each node has a parent pointer and two child pointers left and right.

We could now write getters for those other nodes. Should they return references or pointers to const or non-const Nodes? Should the getters themselves be marked const? Let’s try:

class Node {
  Node* parent;
  Node* left;
  Node* right;
public:
  Node* getParent() const;
  Node* getLeft() const;
  Node* getRight() const;
};

Node const* node = getTree();
Node* leftChild = node->getLeft();
Node* trickedYou = leftChild()->getParent();

Here trickedYou is a non-const pointer to the same const object as node, and we got there by only using const methods. That means the const was in fact a lie. We have to be careful designing our interfaces, by adding const consciously.

A bit too const

There is another case where syntactic const doesn’t do what we liked to. In the last example, we had to add some const to get the semantics right. There are cases where the exact opposite is the case, i.e. where syntactic const is just too much.

Imagine a mesh class in a 3D program. Calculating the volume of such objects might be costly. Depending on the uses we might not want to calculate the volume for every mesh when we construct or alter it, but we might want to store the result once we had to calculate it.

class Mesh {
  vector<Vertex> vertices;
  double volume;
  bool volumeCalculated;
public:
  Mesh( /* ... */ ) 
    : /* ... */ volume{0}, volumeCalculated{false}
  {}

  void change(/* ... */) { volumeCalculated = false; }

  double getVolume() const {
    if (volumeCalculated) {
      return volume;
    }
      
    volume = // calculate...   !!!
    volumeCalculated = true; //!!!
    return volume;
  }
};

This won’t compile, because we are modifying the members volume and volumeCalculated inside a const method. The wrong solution which sadly can be seen very often in the wild is to make getVolume() non-const. As a result, you can’t call getVolume() on const meshes, which in turn results in less meshes being declared const.

The right solution in many cases like this is to declare volume and volumeCalculated as mutable. This keyword basically states that a member may be modified by const member methods, which is exactly what we are doing.

Conclusion

Const correctness is more than just using const everywhere. It’s a part of class design, and in some cases an extra thought or two are needed to get it right.

Previous Post
Next Post

19 Comments



  1. After all these const related tricks I can really understand the people who are using C++ just as C with classes. Don’t use const at all (and other “advanced “tricks”) and it might result in a more elegant and easy to understood code.

    Reply

    1. There are also lots of class related tricks. So, some find C more elegant and easier to understand.
      My point is, that, while you have to pay a bit of effort in learning those features add the tricks associated with them, you then benefit from the positive effects, e.g. the possibility of encapsulation or the compiler detecting things changing where they should not.
      If you don’t need the benefits, you don’t need to use the feature of course. (But be honest and don’t decide you won’t need it only because you don’t want tho use it).

      Reply




  2. I’m not sure I like the idea of “mutable” members. It is fairly hidden in the implementation of the class and is thus not apparent when reading through the public API. Looking at “double getVolume() const”, I would really expect the method to not perform any modifications. An alternative approach would be to remove the “volume” field, “volumentCalculated” field and “getVolume() const” method from the Mesh interface and introduce method “MeshWithVolume calculateVolume() const”. The “MeshWithVolume” would represent the same mesh, but it would be responsible for storing the “volume” field and would provide “double getVolume() const” method to retrieve it. Also, there would be no need for “volumeCalculated” field, as you would know the volume was calculated when you have an instance of the “MeshWithVolume” class.

    Reply

    1. mutable members are (or should be) an implementation detail. In the example, the volume and volumeClaculated fields are not part of the class’ state and therefore not of its visible behavior. You don’t need to care about them, because whether they change or not, getVolume leaves the class in the same state and therefore is semantically a const operation.

      Reply

      1. I see, thank you for clearing that up. If I understand it correctly, modifying the “mutable” member in the “getVolume() const” method will make it not thread-safe, as TX points out. Would you please consider pointing this out in the article? I think without mentioning it, it looks like you get the solution “for free”.

        Reply

  3. Interesting, as I run into the later case on a daily basis. The workaround for years was for me to declare the member mutable, as you said, but recently I started to wonder.

    Since the member is modified in a const context in a limited number of cases (declared dirty or something in a non-const method then computed in only ONE const method – some kind of getter), I’ve used const_cast on it instead of declaring it mutable. This way it can only be modified in a const method with some very explicit warnings around it, and nowhere else. What do you think are the pro and cons of such approach?

    Also, encapsulating the whole thing into a template, some kind of LazyType helper with appropriate semantic.

    Reply

  4. The limits of syntactic const are incomplete. You can have a pointer to a const type:

    int a = 2;
    int const * ptr = &a;
    *ptr = 3; // ERROR
    ptr = &a; // OK

    and a const pointer to a const type:

    int const*const ptr = &a;
    *ptr = 3; // ERROR
    ptr = &a; // ERROR

    Reply

  5. Using the mutable keyword to designate variables that can be modified in const member functions was something I had not known before! There is always something new to learn from reading blogs and articles such as these, even (especially!) if they concern the basics. Thank you, Arne!

    Reply

  6. You forgot to mention that proper const correctness guarantees thread safety. Mutable members must be atomic or protected with a mutex.

    Reply

    1. I did not forget it, I left it for another post 😉 Except for trivial cases I would not go as far as to say that it guarantees thread safety, unless you define “proper” that way. But I definitely agree const correct class design goes a long way towards that goal.

      Reply

        1. With nontrivial I mean e.g. the case of accessing objects through const pointers described in the post. Here the thread safety is not guaranteed by the standard any more, you have to carefully design your classes in a way that makes those cases thread safe again.

          Reply

          1. One should always be careful with the design. I do appreciate that you bring up some traps that one can fall into.

            But if you do it correctly you’ll be safe. Otherwise, if you by mistake let const functions violate thread safety, it can be considered a bug.


    1. Hi Alfredo, thanks for the link! I didn’t know that one before, will look into it.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *