Inapoi
Inainte
Cuprins
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 (covered in Chapter 14).
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 10: 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[44]:
//: 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 typical solution (usually 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:
//: 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;
enum { 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. However, there is no overwhelming
reason that you must choose static const over the enum
hack, and in this book the enum hack is used because it is supported by
more compilers at the time this book was
written.
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 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. logical
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 logical const
(also sometimes called memberwise
const).
Bitwise const means that every bit in the object is permanent, so a
bit image of the object will never change. Logical 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 logical 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
// Better: use C++ explicit cast syntax:
(const_cast<Y*>(this))->i++;
}
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(0), 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 stricter. Of course, the object must be
bitwise-const, rather than logical-const. This is easy to see if
logical 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 Chapter 14) 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.
volatile
The syntax of volatile
is identical to that for const, but
volatile means “This data may change outside the knowledge of the
compiler.” Somehow, the environment is changing the data (possibly through
multitasking, multithreading or interrupts), and volatile tells the
compiler not to make any assumptions about that data, especially during
optimization.
If the compiler says, “I read this
data into a register earlier, and I haven’t touched that register,”
normally it wouldn’t need to read the data again. But if the data is
volatile, the compiler cannot make such an assumption because the data
may have been changed by another process,
and it must reread that data
rather than optimizing the code to remove what would normally be a redundant
read.
You create volatile objects using
the same syntax that you use to create const objects. You can also create
const volatile objects, which can’t be changed by the client
programmer but instead change through some outside agency. Here is an example
that might represent a class associated with some piece of communication
hardware:
//: C08:Volatile.cpp
// The volatile keyword
class Comm {
const volatile unsigned char byte;
volatile unsigned char flag;
enum { bufsize = 100 };
unsigned char buf[bufsize];
int index;
public:
Comm();
void isr() volatile;
char read(int index) const;
};
Comm::Comm() : index(0), byte(0), flag(0) {}
// Only a demo; won't actually work
// as an interrupt service routine:
void Comm::isr() volatile {
flag = 0;
buf[index++] = byte;
// Wrap to beginning of buffer:
if(index >= bufsize) index = 0;
}
char Comm::read(int index) const {
if(index < 0 || index >= bufsize)
return 0;
return buf[index];
}
int main() {
volatile Comm Port;
Port.isr(); // OK
//! Port.read(0); // Error, read() not volatile
} ///:~
As with const, you can use
volatile for data members, member functions, and objects themselves. You
can only call volatile member functions for volatile
objects.
The reason that isr( )
can’t actually be used as an interrupt service routine
is that in a member function, the address of the current
object (this) must be secretly passed, and an ISR generally wants no
arguments at all. To solve this problem, you can make isr( ) a
static member
function,
a subject covered in Chapter 10.
The syntax of volatile is
identical to const, so discussions of the two are often treated together.
The two are referred to in combination as the c-v
qualifier.
Summary
The const keyword gives you the
ability to define objects, function arguments, return values and member
functions as constants, and to eliminate the preprocessor for value substitution
without losing any preprocessor benefits. All this provides a significant
additional form of type checking and safety in your programming. The use of
so-called const correctness
(the use of const
anywhere you possibly can) can be a lifesaver for projects.
Although you can ignore const and
continue to use old C coding practices, it’s there to help you. Chapters
11 and on begin using references heavily, and there you’ll see even more
about how critical it is to use const with function
arguments.
Exercises
Solutions to selected exercises
can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from www.BruceEckel.com.
- Create three const
int values, then add them together to produce a value that determines the
size of an array in an array definition. Try to compile the same code in C and
see what happens (you can generally force your C++ compiler to run as a C
compiler by using a command-line
flag).
- Prove to
yourself that the C and C++ compilers really do treat constants differently.
Create a global const and use it in a global constant expression; then
compile it under both C and
C++.
- Create example
const definitions for all the built-in types and their variants. Use
these in expressions with other consts to make new const
definitions. Make sure they compile
successfully.
- Create
a const definition in a header file, include that header file in two
.cpp files, then compile those files and link them. You should not get
any errors. Now try the same experiment with
C.
- Create a
const whose value is determined at runtime by reading the time when the
program starts (you’ll have to use the <ctime> standard
header). Later in the program, try to read a second value of the time into your
const and see what
happens.
- Create a
const array of char, then try to change one of the
chars.
- Create
an extern const declaration in one file, and put a main( ) in
that file that prints the value of the extern const. Provide an extern
const definition in a second file, then compile and link the two files
together.
- Write two
pointers to const long using both forms of the declaration. Point
one of them to an array of long. Demonstrate that you can increment or
decrement the pointer, but you can’t change what it points
to.
- Write a
const pointer to a double, and point it at an array of
double. Show that you can change what the pointer points to, but you
can’t increment or decrement the
pointer.
- Write a
const pointer to a const object. Show that you can only read the
value that the pointer points to, but you can’t change the pointer or what
it points to.
- Remove
the comment on the error-generating line of code in PointerAssignment.cpp
to see the error that your compiler
generates.
- Create a
character array literal with a pointer that points to the beginning of the
array. Now use the pointer to modify elements in the array. Does your compiler
report this as an error? Should it? If it doesn’t, why do you think that
is?
- Create a
function that takes an argument by value as a const; then try to change
that argument in the function
body.
- Create a
function that takes a float by value. Inside the function, bind a
const float& to the argument, and only use the reference from then on
to ensure that the argument is not
changed.
- Modify
ConstReturnValues.cpp removing comments on the error-causing lines one at
a time, to see what error messages your compiler
generates.
- Modify
ConstPointer.cpp removing comments on the error-causing lines one at a
time, to see what error messages your compiler
generates.
- Make a
new version of ConstPointer.cpp called ConstReference.cpp which
demonstrates references instead of pointers (you may need to look forward to
Chapter 11).
- Modify
ConstTemporary.cpp removing the comment on the error-causing line to see
what error messages your compiler
generates.
- Create a
class containing both a const and a non-const float.
Initialize these using the constructor initializer
list.
- Create a class
called MyString which contains a string and has a constructor that
initializes the string, and a print( ) function. Modify
StringStack.cpp so that the container holds MyString objects, and
main( ) so it prints
them.
- Create a class
containing a const member that you initialize in the constructor
initializer list and an untagged enumeration that you use to determine an array
size.
- In
ConstMember.cpp, remove the const specifier on the member function
definition, but leave it on the declaration, to see what kind of compiler error
message you
get.
- Create a class
with both const and non-const member functions. Create
const and non-const objects of this class, and try calling the
different types of member functions for the different types of
objects.
- Create a
class with both const and non-const member functions. Try to call
a non-const member function from a const member function to see
what kind of compiler error message you
get.
- In
Mutable.cpp, remove the comment on the error-causing line to see what
sort of error message your compiler
produces.
- Modify
Quoter.cpp by making quote( ) a const member function
and lastquote
mutable.
- Create
a class with a volatile data member. Create both volatile and
non-volatile member functions that modify the volatile data
member, and see what the compiler says. Create both volatile and
non-volatile objects of your class and try calling both the
volatile and non-volatile member functions to see what is
successful and what kind of error messages the compiler
produces.
- Create a
class called bird that can fly( ) and a class rock
that can’t. Create a rock object, take its address, and assign that
to a void*. Now take the void*, assign it to a bird*
(you’ll have to use a cast), and call fly( ) through that
pointer. Is it clear why C’s permission to openly assign via a
void* (without a cast) is a “hole” in the language, which
couldn’t be propagated into
C++?
[43]
Some folks go as far as saying that everything in C is pass by value,
since when you pass a pointer a copy is made (so you’re passing the
pointer by value). However precise this might be, I think it actually confuses
the issue.
[44]
At the time of this writing, not all compilers supported this
feature.
 |
|