Feed Icon RSS 1.0 XML Feed available

The transitive power of C++'s const keyword

Date: 10-Feb-2005/16:33

Tags:

Characters: (none)

In my view, one of the most underused aspects of C++ is the idea of const methods and pointers. Like the private and protected keywords, they place a certain form of access control on an object. But unlike private and 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 BCannotTrigger() and BCanTrigger():
class A
   {
   ...
protected:
   // can't let classes derived from B invoke this
   virtual void BCannotTrigger();

public:
   // it's ok for classes derived from B to use this
   virtual void BCanTrigger(); 
   ...
   };

class B
   {
   ...
   // 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
  {
  void BCanTrigger()
     {
     // do some stuff
     ...

     // now call useful routine
     BCannotTrigger();
     } 
  }
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 private and 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 const modifier!
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:
class A
   {
   ...
public:
   virtual void BCannotTrigger();
   virtual void BCanTrigger() const;
   ...
   };

class B
   {
   ...
   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!
Business Card from SXSW
Copyright (c) 2007-2018 hostilefork.com

Project names and graphic designs are All Rights Reserved, unless otherwise noted. Software codebases are governed by licenses included in their distributions. Posts on blog.hostilefork.com are licensed under the Creative Commons BY-NC-SA 4.0 license, and may be excerpted or adapted under the terms of that license for noncommercial purposes.