Const Correctness


By Alex Allain
The const keyword allows you to specify whether or not a variable is modifiable. You can use const to prevent modifications to variables and const pointers and const references prevent changing the data pointed to (or referenced).

But why do you care?





Const gives you the ability to document your program more clearly and actually enforce that documentation. By enforcing your documentation, the const keyword provides guarantees to your users that allow you to make performance optimizations without the threat of damaging their data. For instance, const references allow you to specify that the data referred to won't be changed; this means that you can use const references as a simple and immediate way of improving performance for any function that currently takes objects by value without having to worry that your function might modify the data. Even if it does, the compiler will prevent the code from compiling and alert you to the problem. On the other hand, if you didn't use const references, you'd have no easy way to ensure that your data wasn't modified.

Documentation and Safety

The primary purpose of constness is to provide documentation and prevent programming mistakes. Const allows you to make it clear to yourself and others that something should not be changed. Moreover, it has the added benefit that anything that you declare const will in fact remain const short of the use of forceful methods (which we'll talk about later). It's particularly useful to declare reference parameters to functions as const references:
bool verifyObjectCorrectness (const myObj& obj);
Here, a myObj object is passed by reference into verifyObjectCorrectness. For safety's sake, const is used to ensure that verifyObjectCorrectness cannot change the object--after all, it's just supposed to make sure that the object is in a valid state. This can prevent silly programming mistakes that might otherwise result in damaging the object (for instance, by setting a field of the class for testing purposes, which might result in the field's never being reset). Moreover, by declaring the argument const, users of the function can be sure that their object will not be changed and not need to worry about the possible side effects of making the function call.

Syntax Note

When declaring a const variable, it is possible to put const either before or after the type: that is, both
int const x = 5;
and
const int x = 4;
result in x's being a constant integer. Note that in both cases, the value of the variable is specified in the declaration; there's no way to set it later!

Const Pointers

We've already seen const references demonstrated, and they're pretty natural: when you declare a const reference, you're only making the data referred to const. References, by their very nature, cannot change what they refer to. Pointers, on the other hand, have two ways that you can use them: you can change the data pointed to, or change the pointer itself. Consequently, there are two ways of declaring a const pointer: one that prevents you from changing what is pointed to, and one that prevents you from changing the data pointed to.

The syntax for declaring a pointer to constant data is natural enough:
const int *p_int;
You can think of this as reading that *p_int is a "const int". So the pointer may be changeable, but you definitely can't touch what p_int points to. The key here is that the const appears before the *.

On the other hand, if you just want the address stored in the pointer itself to be const, then you have to put const after the *:
int x;
int * const p_int = &x;
Personally, I find this syntax kind of ugly; but there's not any other obviously better way to do it. The way to think about it is that "* const p_int" is a regular integer, and that the value stored in p_int itself cannot change--so you just can't change the address pointed to. Notice, by the way, that this pointer had to be initialized when it was declared: since the pointer itself is const, we can't change what it points to later on! Them's the rules.

Generally, the first type of pointer, where the data is immutable, is what I'll refer to as a "const pointer" (in part because it's the kind that comes up more often, so we should have a natural way of describing it).

Const Functions

The effects of declaring a variable to be const propagate throughout the program. Once you have a const object, it cannot be assigned to a non-const reference or use functions that are known to be capable of changing the state of the object. This is necessary to enforce the const-ness of the object, but it means you need a way to state that a function should not make changes to an object. In non-object-oriented code, this is as easy as using const references as demonstrated above.

In C++, however, there's the issue of classes with methods. If you have a const object, you don't want to call methods that can change the object, so you need a way of letting the compiler know which methods can be safely called. These methods are called "const functions", and are the only functions that can be called on a const object. Note, by the way, that only member methods make sense as const methods. Remember that in C++, every method of an object receives an implicit this pointer to the object; const methods effectively receive a const this pointer.

The way to declare that a function is safe for const objects is simply to mark it as const; the syntax for const functions is a little bit peculiar because there's only one place where you can really put the const: at the end of the function:
<return-value> <class>::<member-function>(<args>) const
{
        // ...
}
For instance,
int Loan::calcInterest() const
{
        return loan_value * interest_rate; 
}


Note that just because a function is declared const that doesn't prohibit non-const functions from using it; the rule is this:
  • Const functions can always be called
  • Non-const functions can only be called by non-const objects
That makes sense: if you have a const function, all that means is that it guarantees it won't change the object. So just because it is const doesn't mean that non-const objects can't use it.

As a matter of fact, const functions have a slightly stronger restriction than merely that they cannot modify the data. They must make it so that they cannot be used in a way that would allow you to use them to modify const data. This means that when const functions return references or pointers to members of the class, they must also be const.

Const Overloading

In large part because const functions cannot return non-const references to an objects' data, there are many times where it might seem appropriate to have both const and non-const versions of a function. For instance, if you are returning a reference to some member data (usually not a good thing to do, but there are exceptions), then you may want to have a non-const version of the function that returns a non-const reference:
int& myClass::getData()
{
        return data;
}
On the other hand, you don't want to prevent someone using a const version of your object,
myClass constDataHolder;
from getting the data. You just want to prevent that person from changing it by returning a const reference. But you probably don't want the name of the function to change just because you change whether the object is const or not--among other things, this would mean an awful lot of code might have to change just because you change how you declare a variable--going from a non-const to a const version of a variable would be a real headache.

Fortunately, C++ allows you to overload based on the const-ness of a method. So you can have both const and non-const methods, and the correct version will be chosen. If you wish to return a non-const reference in some cases, you merely need to declare a second, const version of the method that returns a const method:
// called for const objects only since a non-const version also exists
const int& myData::getData() const
{
        return data;
}

Const iterators

As we've already seen, in order to enforce const-ness, C++ requires that const functions return only const pointers and references. Since iterators can also be used to modify the underlying collection, when an STL collection is declared const, then any iterators used over the collection must be const iterators. They're just like normal iterators, except that they cannot be used to modify the underlying data. (Since iterators are a generalization of the idea of pointers, this makes sense.)

Const iterators in the STL are simple enough: just append "const_" to the type of iterator you desire. For instance, we could iterator over a vector as follows:
std::vector<int> vec;
vec.push_back( 3 );
vec.push_back( 4 );
vec.push_back( 8 );

for ( std::vector<int>::const_iterator itr = vec.begin(), end = vec.end(); 
      itr != end;
      ++itr )
{
        // just print out the values...
        std::cout<< *itr <<std::endl;
}
Note that I used a const iterator to iterate over a non-const collection. Why do that? For the same reason that we normally use const: it prevents the possibility of silly programming mistakes ("oops, I meant to compare the two values, not assign them!") and it documents that we never intend to use the iterator to change the collection.

Const cast

Sometimes, you have a const variable and you really want to pass it into a function that you are certain won't modify it. But that function doesn't declare its argument as const. (This might happen, for instance, if a C library function like strlen were declared without using const.) Fortunately, if you know that you are safe in passing a const variable into a function that doesn't explicitly indicate that it will not change the data, then you can use a const_cast in order to temporarily strip away the const-ness of the object.

Const casts look like regular typecasts in C++, except that they can only be used for casting away constness (or volatile-ness) but not converting between types or casting down a class hierarchy.
// a bad version of strlen that doesn't declare its argument const
int bad_strlen (char *x)
{
        strlen( x );
}

// note that the extra const is actually implicit in this declaration since
// string literals are constant
const char *x = "abc";

// cast away const-ness for our strlen function 
bad_strlen( const_cast<char *>(x) );
Note that you can also use const_cast to go the other way--to add const-ness--if you really wanted to.

Efficiency Gains? A note about Conceptual vs. Bitwise Constness

One common justification for const correctness is based on the misconception that constness can be used as the basis for optimizations. Unfortunately, this is generally not the case--even if a variable is declared const, it will not necessarily remain unchanged. First, it's possible to cast away constness using a const_cast. It might seem like a silly thing to do when you declare a parameter to a function as const, but it's possible. The second issue is that in classes, even const classes can be changed because of the mutable keyword.

Mutable Data in Const Classes

First, why would you ever want to have the ability to change data in a class that's declared const? This gets at the heart of what constness means, and there are two ways of thinking about it. One idea is that of "bitwise constness", which basically means that a const class should have exactly the same representation in memory at all times. Unfortunately (or fortunately), this is not the paradigm used by the C++ standard; instead, C++ uses "conceptual constness". Conceptual constness refers to the idea that the output of the const class should always be the same. This means that the underlying data might change as long as the fundamental behavior remains the same. (In essence, the "concept" is constant, but the representation may vary.)

Why have conceptual constness?

Why would you ever prefer conceptual constness to bitwise constness? One reason is efficiency: for instance, if your class has a function that relies on a value that takes a long time to calculate, it might be more efficient to calculate the value once and then store it for later requests. This won't change the behavior of the function--it will always return the same value. It will, however, change the representation of the class because it must have some place to cache the value.

C++ Support for Conceptual Constness

C++ provides for conceptual constness by using the mutable keyword: when declaring a class, you may specify that some of the fields are mutable:
mutable int my_cached_result;
this will allow const functions to change the field regardless of whether or not the object itself was declared as const.

Other Ways of Achieving The Same Gains

If you were planning on using const to increase efficiency, think about what this would really mean--it would be akin to using the original data without making a copy of it. But if you wanted to do that, the simplest approach would just be to use references or pointers (preferably const references or pointers). This gives you a real efficiency gain without relying on compiler optimizations that probably aren't there.

Dangers of Too-much Constness

Beware of exploiting const too much; for instance, just because you can return a const reference doesn't mean that you should return a const reference. The most important example is that if you have local data in a function, you really ought not return a reference to it at all (unless it is static) since it will be a reference to memory that is no longer valid.

Another time when returning a const reference may not a good idea is when you are returning a reference to member data of an object. Although returning a const reference prevents anyone from changing the data by using it, it means that you have to have persistent data to back the reference--it has to actually be a field of the object and not temporary data created in the function. Once you make the reference part of the interface to the class, then, you fix the implementation details. This can be frustrating if you later wish to change your class's private data so the result of the function is computed when the function is invoked rather than actually be stored in the class at all times.

Summary

Don't look at const as a means of gaining efficiency so much as a way to document your code and ensure that some things cannot change. Remember that const-ness propagates throughout your program, so you must use const functions, const references, and const iterators to ensure that it would never be possible to modify data that was declared const.

Parts of this article were based on material from More Effective C++: 35 New Ways to Improve Your Programs and Designs by Scott Meyers and Exceptional C++ Style : 40 New Engineering Puzzles, Programming Problems, and Solutions by Herb Sutter.