Classes
This
section shows the ways you can 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 understand the options in order to
create
const
data members of a class.
You
can also make an entire object
const
(and as you’ve just seen, the compiler always makes temporary objects
const).
But preserving the
constness
of an 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
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.
You
might 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 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 an ordinary (non-
static)
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 would be 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. The proper form for
const
inside a class is shown here:
//: C08:ConstInitialization.cpp
// Initializing const in classes
#include <iostream>
using namespace std;
class Fred {
const int size;
public:
Fred(int sz);
void print();
};
Fred::Fred(int sz) : size(sz) {}
void Fred::print() { cout << size << endl; }
int main() {
Fred a(1), b(2), c(3);
a.print(), b.print(), c.print();
The
form of the constructor initializer list shown above is confusing at first
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:
//: C08:BuiltInTypeConstructors.cpp
#include <iostream>
using namespace std;
class B {
int i;
public:
B(int ii);
void print();
};
B::B(int ii) : i(ii) {}
void B::print() { cout << i << endl; }
int main() {
B a(1), b(2);
float pi(3.14159);
a.print(); b.print();
cout << pi << endl;
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, which is why the
float
pi(3.14159)
definition works in the above code.
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:
//: C08:EncapsulatingTypes.cpp
#include <iostream>
using namespace std;
class Integer {
int i;
public:
Integer(int ii = 0);
void print();
};
Integer::Integer(int ii) : i(ii) {}
void Integer::print() { cout << i << ' '; }
int main() {
Integer i[100];
for(int j = 0; j < 100; j++)
i[j].print();
The
array of
Integers
in
main( )
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
The
above use of
const
is interesting and probably useful in cases, but it does not solve the original
problem which is: “how do you make a compile-time constant inside a
class?” The answer requires the use of an additional keyword which will
not be fully introduced until Chapter XX:
static.
The
static
keyword, in this situation, means “there’s only one instance,
regardless of how many objects of the class are created,” which is
precisely what we need here: a member of a class which is constant, and which
cannot change from one object of the class to another. Thus, a
static
const
of
a built-in type can be treated as a compile-time constant.
There
is one feature of
static
const
when used inside classes which is a bit unusual: you must provide the
initializer at the point of definition of the
static
const
.
This is something that only occurs with the
static
const
;
as much as you might like to use it in other situations it won’t work
because all other data members must be initialized in the constructor or in
other member functions.
Here’s
an example that shows the creation and use of a
static
const
called
size
inside a class that represents a stack of string pointers:
//: C08:StringStack.cpp
// Using static const to create a compile-time
// constant inside a class
#include <string>
#include <iostream>
using namespace std;
class StringStack {
static const int size = 100;
const string* stack[size];
int index;
public:
StringStack();
void push(const string* s);
const string* pop();
};
StringStack::StringStack() : index(0) {
memset(stack, 0, size * sizeof(string*));
}
void StringStack::push(const string* s) {
if(index < size)
stack[index++] = s;
}
const string* StringStack::pop() {
if(index > 0) {
const string* rv = stack[--index];
stack[index] = 0;
return rv;
}
return 0;
}
string 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 string* cp;
while((cp = ss.pop()) != 0)
cout << *cp << endl;
Since
size
is used to determine the size of the array
stack,
it is indeed a compile-time constant, but one that is hidden inside the class.
Notice
that
push( )
takes a
const
string*
as an argument,
pop( )
returns a
const
string*,
and
StringStack
holds
const
string*
.
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.
The
“enum hack” in old code
In
older versions of C++,
static
const
was not supported inside classes.
This meant that
const
was useless for constant expressions inside classes. However, people still
wanted to do this so a common solution (typically referred to as the
“enum hack”) was 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 (in older code
[32]):
//: C08:EnumHack.cpp
#include <iostream>
using namespace std;
class Bunch {
enum { size = 1000 };
int i[size];
};
int main() {
cout << "sizeof(Bunch) = " << sizeof(Bunch) <<
", sizeof(i[1000]) = " << sizeof(int[1000])
<< endl;
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.
In
the
StringStack.cpp
example above, the line:
static
const int size = 100;
Although
you’ll often see the
enum
technique in legacy code, the
static
const
feature was added to the language to solve just this problem and it produces a
more flexible compile-time constant inside a class.
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;
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 member function is
const
doesn’t guarantee it 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.
To
understand the syntax for declaring
const
member
functions, first notice that preceding the function declaration with
const
means the return value is
const,
so that doesn’t produce the desired results. Instead, you must place the
const
specifier
after
the argument list. For example,
//: C08:ConstMember.cpp
class X {
int i;
public:
X(int ii);
int f() const;
};
X::X(int ii) : i(ii) {}
int X::f() const { return i; }
int main() {
X x1(10);
const X x2(20);
x1.f();
x2.f();
Note
that the
const
keyword must be repeated in the definition or the compiler sees it as a
different function. Since
f( )
is a
const
member function, if it attempts to change
i
in any way
or
to call another member function that is not
const,
the compiler flags it as an error.
You
can see that a
const
member function is safe to call with both
const
and non-
const
objects. Thus, you could think of it as the most general form of a member
function (and because of this, it is unfortunate that member functions do not
automatically default to
const).
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",
"Things that make us happy, make us wise",
};
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
(see 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 to ensure bitwise
constness.
To effect memberwise
constness,
there are two ways to change a data member from within 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 cast it to a
pointer to an object of the current type. It would seem that
this
is
already
such a pointer. However, inside a
const
member
function it’s actually 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;
public:
Y();
void f() const;
};
Y:: Y() { i = 0; }
void Y::f() const {
//! i++; // Error -- const member function
((Y*)this)->i++; // 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 definition, and you have no clue from the
class interface that the data of the object is actually being modified unless
you have access to the source code (and you must suspect that
constness
is being cast away, and look for the cast). 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 Z {
int i;
mutable int j;
public:
Z();
void f() const;
};
Z::Z() { i = j = 0; }
void Z::f() const {
//! i++; // Error -- const member function
j++; // OK: mutable
}
int main() {
const Z zz;
zz.f(); // Actually changes it!
This
way, 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.
[32]
If your compiler doesn’t support
static
const
,
you’ll have to replace all the instances in this book of
static
const
inside classes with the enum hack in order to get them to compile.
Contact: webmaster@codeguru.com
CodeGuru - the website for developers.