Classes
This
section shows the two ways to use
const
with classes. You may want to create a local
const
in a class to use inside constant expressions that will be evaluated at compile
time. However, the meaning of
const
is different inside classes, so you must use an alternate technique with
enumerations to achieve the same effect.
You
can also make a class object
const
(and as you’ve just seen, the compiler always makes temporary class
objects
const).
But preserving the
constness
of a class object is more complex. The compiler can ensure the
constness
of a built-in type but it cannot monitor the intricacies of a class. To
guarantee the
constness
of a class object, the
const
member function is introduced: Only a
const
member function may
be called for a
const
object.
const
and enum in classes
One
of the places you’d like to use a
const
for constant expressions is inside classes. The typical example is when
you’re creating an array inside a class and you want to use a
const
instead of a
#define
to establish the array size and to use in calculations involving the array. The
array size is something you’d like to keep hidden inside the class, so if
you used a name like
size,
for example, you could use that name in another class without a clash. The
preprocessor treats all
#defines
as global from the point they are defined, so this will not achieve the desired
effect.
Initially,
you probably assume that the logical choice is to place a
const
inside the class. This doesn’t produce the desired result. Inside a class,
const
partially reverts to its meaning in C. It allocates storage within each class
object and represents a value that is initialized once and then cannot change.
The use of
const
inside a class means “This is constant for the lifetime of the
object.” However, each different object may contain a different value for
that constant.
Thus,
when you create a
const
inside a class, you cannot give it an initial value. This initialization must
occur in the constructor, of course, but in a special place in the constructor.
Because a
const
must be initialized at the point it is created, inside the main body of the
constructor the
const
must
already
be
initialized. Otherwise you’re left with the choice of waiting until some
point later in the constructor body, which means the
const
would be un-initialized for a while. Also, there’s nothing to keep you
from changing the value of the
const
at various places in the constructor body.
The
constructor initializer list
The
special initialization point is called the
constructor
initializer list,
and it was originally developed for use in inheritance (an object-oriented
subject of a later chapter). The constructor initializer list – which, as
the name implies, occurs only in the definition of the constructor – is a
list of “constructor calls” that occur after the function argument
list and a colon, but before the opening brace of the constructor body. This is
to remind you that the initialization in the list occurs before any of the main
constructor code is executed. This is the place to put all
const
initializations, so the proper form for
const
inside a class is
class Fred {
const int size;
public:
Fred();
};
Fred::Fred() : size(100) {}
The
form of the constructor initializer list shown above is at first confusing
because you’re not used to seeing a built-in type treated as if it has a
constructor.
“Constructors”
for built-in types
As
the language developed and more effort was put into making user-defined types
look like built-in types, it became apparent that there were times when it was
helpful to make built-in types look like user-defined types. In the constructor
initializer list, you can treat a built-in type as if it has a constructor,
like this:
class B {
int i;
public:
B(int ii);
};
B::B(int ii) : i(ii) {}
This
is especially critical when initializing
const
data members because
they must be initialized before the function body is entered.
It
made sense to extend this “constructor” for built-in types (which
simply means assignment) to the general case. Now you can say
It’s
often useful to encapsulate a built-in type inside a class to guarantee
initialization with the constructor. For example, here’s an
Integer
class:
class Integer {
int i;
public:
Integer(int ii = 0);
};
Integer::Integer(int ii) : i(ii) {}
Now
if you make an array of
integers,
they are all automatically initialized to zero:
This
initialization isn’t
necessarily more costly than a
for
loop or
memset( ).
Many compilers easily optimize this to a very fast process.
Compile-time
constants in classes
Because
storage is allocated in the class object, the compiler cannot know what the
contents of the
const
are, so it cannot be used as a compile-time constant.
This means that, for constant expressions inside classes,
const
becomes as useless as it is in C. You cannot say
class Bob {
const int size = 100; // Illegal
int array[size]; // Illegal
//...
The
meaning of
const
inside a class is “This value is
const
for the lifetime of this particular object, not for the class as a
whole.” How then do you create a class constant that can be used in
constant expressions? A common solution is to use an untagged
enum
with
no instances. An enumeration must have all its values established at compile
time, it’s local to the class, and its values are available for constant
expressions. Thus, you will commonly see
class Bunch {
enum { size = 1000 };
int i[size];
};
The
use of
enum
here is guaranteed to occupy no storage in the object, and the enumerators are
all evaluated at compile time. You can also explicitly establish the values of
the enumerators:
enum
{ one = 1, two = 2, three };
With
integral
enum
types, the compiler will continue counting from the last value, so the
enumerator
three
will get the value 3.
Here’s
an example that shows the use of
enum
inside a container that represents a
Stack
of string pointers:
//: C08:SStack.cpp
// enum inside classes
#include <cstring>
#include <iostream>
using namespace std;
class StringStack {
enum { size = 100 };
const char* Stack[size];
int index;
public:
StringStack();
void push(const char* s);
const char* pop();
};
StringStack::StringStack() : index(0) {
memset(Stack, 0, size * sizeof(char*));
}
void StringStack::push(const char* s) {
if(index < size)
Stack[index++] = s;
}
const char* StringStack::pop() {
if(index > 0) {
const char* rv = Stack[--index];
Stack[index] = 0;
return rv;
}
return 0;
}
const char* iceCream[] = {
"pralines & cream",
"fudge ripple",
"jamocha almond fudge",
"wild mountain blackberry",
"raspberry sorbet",
"lemon swirl",
"rocky road",
"deep chocolate fudge"
};
const int iCsz =
sizeof iceCream / sizeof *iceCream;
int main() {
StringStack SS;
for(int i = 0; i < iCsz; i++)
SS.push(iceCream[i]);
const char* cp;
while((cp = SS.pop()) != 0)
cout << cp << endl;
} ///:~
Notice
that
push( )
takes a
const
char*
as an argument,
pop( )
returns a
const
char*,
and
Stack
holds
const
char*
.
If this were not true, you couldn’t use a
StringStack
to hold the pointers in
iceCream.
However, it also prevents you from doing anything that will change the objects
contained by
StringStack.
Of course, not all containers are designed with this restriction.
Although
you’ll often see the
enum
technique in legacy code, C++ also has the
static
const
which produces a more flexible compile-time constant inside a class. This is
described in Chapter 8.
Type
checking for enumerations
C’s
enumerations are
fairly primitive, simply associating integral values with names, but providing
no type checking. In C++, as you may have come to expect by now, the concept of
type is fundamental, and this is true with enumerations. When you create a
named enumeration, you effectively create a new type just as you do with a
class: The name of your enumeration becomes a reserved word for the duration of
that translation unit.
In
addition, there’s stricter type checking for enumerations in C++ than in
C. You’ll notice this in particular if you have an instance of an
enumeration color
called
a.
In C you can say
a++
but in C++ you can’t. This is because incrementing an enumeration is
performing two type conversions, one of them legal in C++ and one of them
illegal. First, the value of the enumeration is implicitly cast from a
color
to an
int,
then the value is incremented, then the
int
is cast back into a
color.
In C++ this isn’t allowed, because
color
is a distinct type and not equivalent to an
int.
This makes sense because how do you know the increment of
blue
will even be in the list of colors? If you want to increment a
color,
then it should be a class (with an increment operation) and not an
enum.
Any time you write code that assumes an implicit conversion to an
enum
type, the compiler will flag this inherently dangerous activity.
Unions
have
similar additional type checking.
const
objects & member functions
Class
member functions can be made
const.
What does this mean? To understand, you must first grasp the concept of
const
objects.
A
const
object is defined the same for a user-defined type as a built-in type. For
example:
const int i = 1;
const blob B(2);
Here,
B
is a
const
object of type
blob.
Its constructor is called with an argument of two. For the compiler to enforce
constness,
it must ensure that no data members of the object are changed during the
object’s lifetime. It can easily ensure that no public data is modified,
but how is it to know which member functions will change the data and which
ones are “safe” for a
const
object?
If
you declare a member function
const,
you tell the compiler the function can be called for a
const
object. A member function that is not specifically declared
const
is treated as one that will modify data members in an object, and the compiler
will not allow you to call it for a
const
object.
It
doesn’t stop there, however. Just
claiming
a function is
const
inside a class definition doesn’t guarantee the member function
definition will act that way, so the compiler forces you to reiterate the
const
specification when defining the function. (The
const
becomes part of the function signature, so both the compiler and linker check
for
constness.)
Then it enforces
constness
during the function definition by issuing an error message if you try to change
any members of the object
or
call a non-
const
member function. Thus, any member function you declare
const
is guaranteed to behave that way in the definition.
Preceding
the function declaration with
const
means the return value is
const,
so that isn’t the proper syntax. You must place the
const
specifier
after
the argument list. For example,
class X {
int i;
public:
int f() const;
};
The
const
keyword must be repeated in the definition using the same form, or the compiler
sees it as a different function:
int
X::f() const { return i; }
If
f( )attempts
to change
i
in any way
or
to call another member function that is not
const,
the compiler flags it as an error.
Any
function that doesn’t modify member data should be declared as
const,
so it can be used with
const
objects.
Here’s
an example that contrasts a
const
and non-
const
member function:
//: C08:Quoter.cpp
// Random quote selection
#include <iostream>
#include <cstdlib> // Random number generator
#include <ctime> // To seed random generator
using namespace std;
class Quoter {
int lastquote;
public:
Quoter();
int Lastquote() const;
const char* quote();
};
Quoter::Quoter(){
lastquote = -1;
srand(time(0)); // Seed random number generator
}
int Quoter::Lastquote() const {
return lastquote;
}
const char* Quoter::quote() {
static const char* quotes[] = {
"Are we having fun yet?",
"Doctors always know best",
"Is it ... Atomic?",
"Fear is obscene",
"There is no scientific evidence "
"to support the idea "
"that life is serious",
};
const int qsize = sizeof quotes/sizeof *quotes;
int qnum = rand() % qsize;
while(lastquote >= 0 && qnum == lastquote)
qnum = rand() % qsize;
return quotes[lastquote = qnum];
}
int main() {
Quoter q;
const Quoter cq;
cq.Lastquote(); // OK
//! cq.quote(); // Not OK; non const function
for(int i = 0; i < 20; i++)
cout << q.quote() << endl;
} ///:~
Neither
constructors nor destructors can be
const
member functions because they virtually always perform some modification on the
object during initialization and cleanup. The
quote( )
member function also cannot be
const
because it modifies the data member
lastquote
in the return statement. However,
Lastquote( )
makes no modifications, and so it can be
const
and can be safely called for the
const
object
cq.
mutable:
bitwise vs. memberwise const
What
if you want to create a
const
member function, but you’d still like to change some of the data in the
object? This is sometimes referred to as the difference between
bitwise
const
and
memberwise
const.
Bitwise
const
means that every bit in the object is permanent, so a bit image of the object
will never change. Memberwise
const
means that, although the entire object is conceptually constant, there may be
changes on a member-by-member basis. However, if the compiler is told that an
object is
const,
it will jealously guard that object. There are two ways to change a data member
inside a
const
member function.
The
first approach is the historical one and is called
casting
away constness.
It is performed in a rather odd fashion. You take
this
(the keyword that produces the address of the current object) and you cast it
to a pointer to an object of the current type. It would seem that
this
is
already
such a pointer, but it’s a
const
pointer, so by casting it to an ordinary pointer, you remove the
constness
for that operation. Here’s an example:
//: C08:Castaway.cpp
// "Casting away" constness
class Y {
int i, j;
public:
Y() { i = j = 0; }
void f() const;
};
void Y::f() const {
//! i++; // Error -- const member function
((Y*)this)->j++; // OK: cast away const-ness
}
int main() {
const Y yy;
yy.f(); // Actually changes it!
} ///:~
This
approach works and you’ll see it used in legacy code, but it is not the
preferred technique. The problem is that this lack of
constness
is hidden away in a member function of an object, so the user has no clue that
it’s happening unless she has access to the source code (and actually
goes looking for it). To put everything out in the open, you should use the
mutable
keyword in the class declaration to specify that a particular data member may
be changed inside a
const
object:
//: C08:Mutable.cpp
// The "mutable" keyword
class Y {
int i;
mutable int j;
public:
Y() { i = j = 0; }
void f() const;
};
void Y::f() const {
//! i++; // Error -- const member function
j++; // OK: mutable
}
int main() {
const Y yy;
yy.f(); // Actually changes it!
} ///:~
Now
the user of the class can see from the declaration which members are likely to
be modified in a
const
member function.
ROMability
If
an object is defined as
const,
it is a candidate to be placed in read-only memory (ROM),
which is often an important consideration in embedded systems programming.
Simply making an object
const,
however, is not enough – the requirements for ROMability are
much more strict. Of course, the object must be bitwise-
const,
rather than memberwise-
const.
This is easy to see if memberwise
constness
is implemented only through the
mutable
keyword, but probably not detectable by the compiler if
constness
is cast away inside a
const
member function. In addition,
- The
class
or
struct
must have no user-defined constructors or destructor.
- There
can be no base classes (covered in the future chapter on inheritance) or member
objects with user-defined constructors or destructors.
The
effect of a write operation on any part of a
const
object of a ROMable type is undefined. Although a suitably formed object may be
placed in ROM, no objects are ever
required
to be placed in ROM.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru