Contents
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<T> const tVec
, the access operator tVec[0]
returns a T const&
, 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.
Permalink
Permalink
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.
Permalink
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).
Permalink
Permalink
Permalink
Permalink
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.
Permalink
mutable members are (or should be) an implementation detail. In the example, the
volume
andvolumeClaculated
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.Permalink
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”.
Permalink
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.
Permalink
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
Permalink
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!
Permalink
You forgot to mention that proper const correctness guarantees thread safety. Mutable members must be atomic or protected with a mutex.
Permalink
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.
Permalink
No, not only for trivial cases. The meaning of const changed with C++11. It’s guaranteed by the standard.
For a deeper explanation:
https://herbsutter.com/2013/05/24/gotw-6a-const-correctness-part-1-3/
https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Permalink
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.
Permalink
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.
Permalink
Maybe it is a good point to mention
std::experimental::propagrate_const
which is a way of propagating the constness into internal smart and raw pointers. I guess this helps solving the “Not const enough” problem. http://en.cppreference.com/w/cpp/experimental/propagate_constPermalink
Hi Alfredo, thanks for the link! I didn’t know that one before, will look into it.