C++ Tips: 3 - Strive to be const correct

| 0 Comments

Another extremely powerful tool that you can use to ensure that your C++ code communicates as clearly as possible is const. By correctly using const all the time when designing your abstractions you can divide an object's interface into two smaller, easier to understand interfaces; one which does change the object's internal state and one which doesn't. By correctly using const all the time when defining constants and variables you can clearly communicate which are which.

Strive to be const correct.

const isn't just for passing objects to functions as references that can't be changed:

CWidget &CWidgets::Get(const std::string &name)

Although looking at much C++ code you might think it was.

The first, and simplest, use of const to improve code clarity is simply using it when defining variables that never change, constants. Say we have a function that calls a few other functions to obtain the data that it needs before doing its main work. If that data isn't changed by the function then it should be declared as const. This clearly communicates that the function doesn't change the data later on and it allows the compiler to protect you from accidental attempts to change the data. Seeing const means that you can mentally flag the value as a constant and that's one less thing to worry about. If a value would normally be set using an if statement but the result should be const then consider using the conditional operator (?:) instead.

Likewise, if parts of your object can never be changed after construction then they should be const so that a) they communicate the fact that they're immutable and b) so that they can't be changed by mistake. It's relatively simple to do, you burn in some of the business rules into the code and you prevent bugs. These parts of the object are not variable they're constant. If your object represents a trade and a trade has an ID that is assigned once and never ever changes then the trade ID should probably be constant within the trade object. The one down side is that you can't assign to the object as the left hand side object in the assignment can't be changed because it contains constant data; in practice I haven't found this to be a limiting factor in most cases, if it is then you can either use the "handle/body" idiom to allow you to maintain the constantness of the body whilst allowing assignment between handles or simply always work in terms of pointers or references to the object in question.

Finally you should use const in the interfaces to your classes so that you separate the interface into two, one that will modify the object and one that cannot. If a method is a query on the object and simply returns details of the object's state and doesn't change that state then the method should be declared const.

bool CLocationManager::Contains(
   LocationIndex location) const
{
   return m_callStacks.find(location) != m_callStacks.end();     
}

This serves three purposes; i) it communicates precisely what's going on, ii) it allows the compiler to prevent accidental mistakes within the body of the method, iii) it allows the method to be used on a constant instance of the object (which is extremely important if you're correctly declaring your constants to be const!).

The correct use of const is contagious. Once you get into the habit of using const to clearly declare your constants you'll find that you have to use const to correctly define your interfaces.

If you have a class that holds a reference to a collection that it never changes then this reference should be const. Once it is const the interface on the class must be correctly defined to provide both a query interface that is const and an "adjustment" interface that is not const. Part of the query inteface might look like this:

const CFunctionStack &CLocationManager::Get(
   LocationIndex location) const
{
   FunctionStacks::const_iterator it = m_functionStacks.find(location);

   if (it == m_functionStacks.end())
   {
      throw CException(_T("CLocationManager::Get()"), _T("Location: ") + ToString(location) + _T(" not found"));
   }

   return *(it->second);
}

Notice how this allows us to both limit what the function can do to the object and clearly communicate that limitation. The function can be used on a constant collection of locations as it cannot change that collection. Likewise it returns a constant reference to one of its contained objects. The caller has a "read-only" view of the collection. You can tell, quickly and accurately by looking at either the object in question or the start of the function if data will be changed.

As I've said before, being const correct means you need to think a little bit more when you write the code but you can trade that for thinking a little bit less when you read the code. Since code is read more times than it's written it's well worth being disciplined when you write code.

Updated 5/1/6 : for the chapter and verse on const correctness see the C++ FAQ Lite.

Leave a comment