Const Correctness
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&tl;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. |