In my view, one of the most underused aspects of C++ is the idea of
const methods and pointers. Like the
protected keywords, they place a certain form of access control on an object. But unlike
protected, they carry this element of control deeply through the call graph.
Imagine that you are designing an architecture where you have base classes A and B. Objects derived from B are not supposed to be able to access the
BCannotTrigger() method of A objects...however they must be able to use the
BCanTrigger() method. Objects derived from A need access to both
// can't let classes derived from B invoke this
virtual void BCannotTrigger();
// it's ok for classes derived from B to use this
virtual void BCanTrigger();
// implement in your derived class
virtual void Callback(A& aInput) = 0;
This looks good on first inspection, but it offers a rather "shallow" protection. Imagine a class derived from A:
class SubclassOfA : public A
// do some stuff
// now call useful routine
Unwittingly, the programmer has given B a back door into A. If you think it should be obvious that they were making a mistake, bear in mind that I gave these methods ridiculous names to make a point--in reality they'll be named based on what they do, rather than who can call them. Secondly, this call could be much deeper inside a subroutine...so even if there was fair warning that this was not supposed to happen, someone could lose track.
I am frequently interested in stopping these "deep" transitive violations of access privileges, as opposed to the "shallow" questions of direct calls. Breaking it into more classes with more
protected bits won't help--crossing the method call boundary will throw away the crucial context information.
Of course C++ is not a pure functional language. Even so, it's possible to put in run-time checks which catch this sort of violation. C++ offers us an oddly-shaped tool for attacking this at compile-time: the
Most people know
const can be used to declare constant values in C++:
const float pi = 3.14; // three digit approximation will suffice
But when you start putting the keyword on methods, there's a neat check the compiler does. A client holding a
const pointer can't call any methods for that class that aren't marked with
const. This applies transitively, because the body of a method marked with
const can't call methods on itself that aren't also marked with
const. Let's take a look at how this might be a way of solving the situation described above:
virtual void BCannotTrigger();
virtual void BCanTrigger() const;
virtual void Callback(const A& aInput) = 0;
Suddenly, we are protected from the implementation of
BCanTrigger() ever accidentally using
BCannotTrigger(). It doesn't matter how convoluted the call graph gets--the pointer that B has is fundamentally incapable of ever being the kind of pointer that can be used to generate a downstream call to
BCannotTrigger()! Plus, the
const property can be applied to each individual variable and parameter in your program, so you can completely control the granularity of these privileges.
Yet I said it was an "oddly-shaped" tool. Contractually we must recognize that C++ programmers expect that
const objects don't change "essential state" in-between method calls. This idea is semi-useful for optimization and documentation, yet it's nowhere near as powerful as the compile-time contract validation demonstrated above--which most developers don't think about. That's why so many of them think
const wastes time and don't use it--they just haven't seen what it can really do!
If we wanted to be sneaky, we could make all our member variables
volatile. Then we could use
const and non-
const to represent a completely arbitrary "object mode bit"--checked and enforced in the transitive call graph by the compiler. I've been tempted to do this and throw the whole notion of "const is for constantness" out the window...but that would be a bad practice given the understandings already established in the C++ community. So don't do that!
However, when you create a Widget, consider making a strong and interesting choice about what a
const or non-
const Widget might conceptually represent. Think big: a
const TextFile object could have read-only access, while a non-
const one could have read/write-access. Examples like this are unconventional but not far-fetched, and your architecture can really start enforcing deep contracts at compile time (without the explosion of parallel code and client hassle that happens with separate ReadOnlyTextFile and ReadWriteTextFile objects).
With a little cleverness and a little luck, you might get much more mileage out of
const than you ever expected!