ZeePedia

Constants:Function arguments & return values, Classes, volatile

<< Function Overloading & Default Arguments:Overloading example, Default arguments
Inline Functions:Preprocessor pitfalls, Stash & Stack with inlines, Reducing clutter >>
img
8: Constants
The concept of constant (expressed by the const
keyword) was created to allow the programmer to
draw a line between what changes and what doesn't.
This provides safety and control in a C++
programming project.
353
img
Since its origin, const has taken on a number of different purposes.
In the meantime it trickled back into the C language where its
meaning was changed. All this can seem a bit confusing at first, and
in this chapter you'll learn when, why, and how to use the const
keyword. At the end there's a discussion of volatile which is a near
,
cousin to const (because they both concern change) and has
identical syntax.
The first motivation for const seems to have been to eliminate the
use of preprocessor #defines for value substitution. It has since
been put to use for pointers, function arguments, return types, class
objects and member functions. All of these have slightly different
but conceptually compatible meanings and will be looked at in
separate sections in this chapter.
Value substitution
When programming in C, the preprocessor is liberally used to
create macros and to substitute values. Because the preprocessor
simply does text replacement and has no concept nor facility for
type checking, preprocessor value substitution introduces subtle
problems that can be avoided in C++ by using const values.
The typical use of the preprocessor to substitute values for names
in C looks like this:
#define BUFSIZE 100
BUFSIZE is a name that only exists during preprocessing, therefore
it doesn't occupy storage and can be placed in a header file to
provide a single value for all translation units that use it. It's very
important for code maintenance to use value substitution instead of
so-called "magic numbers." If you use magic numbers in your
code, not only does the reader have no idea where the numbers
come from or what they represent, but if you decide to change a
value, you must perform hand editing, and you have no trail to
354
Thinking in C++
img
follow to ensure you don't miss one of your values (or accidentally
change one you shouldn't).
Most of the time, BUFSIZE will behave like an ordinary variable,
but not all the time. In addition, there's no type information. This
can hide bugs that are very difficult to find. C++ uses const to
eliminate these problems by bringing value substitution into the
domain of the compiler. Now you can say
const int bufsize = 100;
You can use bufsize anyplace where the compiler must know the
value at compile time. The compiler can use bufsize to perform
constant folding, which means the compiler will reduce a
complicated constant expression to a simple one by performing the
necessary calculations at compile time. This is especially important
in array definitions:
char buf[bufsize];
You can use const for all the built-in types (char, int, float, and
double) and their variants (as well as class objects, as you'll see
later in this chapter). Because of subtle bugs that the preprocessor
might introduce, you should always use const instead of #define
value substitution.
const in header files
To use const instead of #define, you must be able to place const
definitions inside header files as you can with #define. This way,
you can place the definition for a const in a single place and
distribute it to translation units by including the header file. A
const in C++ defaults to internal linkage; that is, it is visible only
within the file where it is defined and cannot be seen at link time by
other translation units. You must always assign a value to a const
when you define it, except when you make an explicit declaration
using extern:
8: Constants
355
img
extern const int bufsize;
Normally, the C++ compiler avoids creating storage for a const, but
instead holds the definition in its symbol table. When you use
extern with const, however, you force storage to be allocated (this
is also true for certain other cases, such as taking the address of a
const). Storage must be allocated because extern says "use external
linkage," which means that several translation units must be able to
refer to the item, which requires it to have storage.
In the ordinary case, when extern is not part of the definition, no
storage is allocated. When the const is used, it is simply folded in at
compile time.
The goal of never allocating storage for a const also fails with
complicated structures. Whenever the compiler must allocate
storage, constant folding is prevented (since there's no way for the
compiler to know for sure what the value of that storage is ­ if it
could know that, it wouldn't need to allocate the storage).
Because the compiler cannot always avoid allocating storage for a
const, const definitions must default to internal linkage, that is,
linkage only within that particular translation unit. Otherwise,
linker errors would occur with complicated consts because they
cause storage to be allocated in multiple cpp files. The linker would
then see the same definition in multiple object files, and complain.
Because a const defaults to internal linkage, the linker doesn't try to
link those definitions across translation units, and there are no
collisions. With built-in types, which are used in the majority of
cases involving constant expressions, the compiler can always
perform constant folding.
Safety consts
The use of const is not limited to replacing #defines in constant
expressions. If you initialize a variable with a value that is
produced at runtime and you know it will not change for the
356
Thinking in C++
img
lifetime of that variable, it is good programming practice to make it
a const so the compiler will give you an error message if you
accidentally try to change it. Here's an example:
//: C08:Safecons.cpp
// Using const for safety
#include <iostream>
using namespace std;
const int i = 100;  // Typical constant
const int j = i + 10; // Value from const expr
long address = (long)&j; // Forces storage
char buf[j + 10]; // Still a const expression
int main() {
cout << "type a character & CR:";
const char c = cin.get(); // Can't change
const char c2 = c + 'a';
cout << c2;
// ...
} ///:~
You can see that i is a compile-time const, but j is calculated from i.
However, because i is a const, the calculated value for j still comes
from a constant expression and is itself a compile-time constant.
The very next line requires the address of j and therefore forces the
compiler to allocate storage for j. Yet this doesn't prevent the use of
j in the determination of the size of buf because the compiler
knows j is const and that the value is valid even if storage was
allocated to hold that value at some point in the program.
In main( ), you see a different kind of const in the identifier c
because the value cannot be known at compile time. This means
storage is required, and the compiler doesn't attempt to keep
anything in its symbol table (the same behavior as in C). The
initialization must still happen at the point of definition, and once
the initialization occurs, the value cannot be changed. You can see
that c2 is calculated from c and also that scoping works for consts
as it does for any other type ­ yet another improvement over the
use of #define.
8: Constants
357
img
As a matter of practice, if you think a value shouldn't change, you
should make it a const. This not only provides insurance against
inadvertent changes, it also allows the compiler to generate more
efficient code by eliminating storage and memory reads.
Aggregates
It's possible to use const for aggregates, but you're virtually
assured that the compiler will not be sophisticated enough to keep
an aggregate in its symbol table, so storage will be allocated. In
these situations, const means "a piece of storage that cannot be
changed." However, the value cannot be used at compile time
because the compiler is not required to know the contents of the
storage at compile time. In the following code, you can see the
statements that are illegal:
//: C08:Constag.cpp
// Constants and aggregates
const int i[] = { 1, 2, 3, 4 };
//! float f[i[3]]; // Illegal
struct S { int i, j; };
const S s[] = { { 1, 2 }, { 3, 4 } };
//! double d[s[1].j]; // Illegal
int main() {} ///:~
In an array definition, the compiler must be able to generate code
that moves the stack pointer to accommodate the array. In both of
the illegal definitions above, the compiler complains because it
cannot find a constant expression in the array definition.
Differences with C
Constants were introduced in early versions of C++ while the
Standard C specification was still being finished. Although the C
committee then decided to include const in C, somehow it came to
mean for them "an ordinary variable that cannot be changed." In C,
a const always occupies storage and its name is global. The C
compiler cannot treat a const as a compile-time constant. In C, if
you say
358
Thinking in C++
img
const int bufsize = 100;
char buf[bufsize];
you will get an error, even though it seems like a rational thing to
do. Because bufsize occupies storage somewhere, the C compiler
cannot know the value at compile time. You can optionally say
const int bufsize;
in C, but not in C++, and the C compiler accepts it as a declaration
indicating there is storage allocated elsewhere. Because C defaults
to external linkage for consts, this makes sense. C++ defaults to
internal linkage for consts so if you want to accomplish the same
thing in C++, you must explicitly change the linkage to external
using extern:
extern const int bufsize; // Declaration only
This line also works in C.
In C++, a const doesn't necessarily create storage. In C a const
always creates storage. Whether or not storage is reserved for a
const in C++ depends on how it is used. In general, if a const is
used simply to replace a name with a value (just as you would use
a #define), then storage doesn't have to be created for the const. If
no storage is created (this depends on the complexity of the data
type and the sophistication of the compiler), the values may be
folded into the code for greater efficiency after type checking, not
before, as with #define. If, however, you take an address of a const
(even unknowingly, by passing it to a function that takes a
reference argument) or you define it as extern, then storage is
created for the const.
In C++, a const that is outside all functions has file scope (i.e., it is
invisible outside the file). That is, it defaults to internal linkage.
This is very different from all other identifiers in C++ (and from
const in C!) that default to external linkage. Thus, if you declare a
const of the same name in two different files and you don't take the
8: Constants
359
img
address or define that name as extern, the ideal C++ compiler
won't allocate storage for the const, but simply fold it into the code.
Because const has implied file scope, you can put it in C++ header
files with no conflicts at link time.
Since a const in C++ defaults to internal linkage, you can't just
define a const in one file and reference it as an extern in another
file. To give a const external linkage so it can be referenced from
another file, you must explicitly define it as extern, like this:
extern const int x = 1;
Notice that by giving it an initializer and saying it is extern, you
force storage to be created for the const (although the compiler still
has the option of doing constant folding here). The initialization
establishes this as a definition, not a declaration. The declaration:
extern const int x;
in C++ means that the definition exists elsewhere (again, this is not
necessarily true in C). You can now see why C++ requires a const
definition to have an initializer: the initializer distinguishes a
declaration from a definition (in C it's always a definition, so no
initializer is necessary). With an extern const declaration, the
compiler cannot do constant folding because it doesn't know the
value.
The C approach to const is not very useful, and if you want to use a
named value inside a constant expression (one that must be
evaluated at compile time), C almost forces you to use #define in
the preprocessor.
Pointers
Pointers can be made const. The compiler will still endeavor to
prevent storage allocation and do constant folding when dealing
with const pointers, but these features seem less useful in this case.
360
Thinking in C++
img
More importantly, the compiler will tell you if you attempt to
change a const pointer, which adds a great deal of safety.
When using const with pointers, you have two options: const can
be applied to what the pointer is pointing to, or the const can be
applied to the address stored in the pointer itself. The syntax for
these is a little confusing at first but becomes comfortable with
practice.
Pointer to const
The trick with a pointer definition, as with any complicated
definition, is to read it starting at the identifier and work your way
out. The const specifier binds to the thing it is "closest to." So if you
want to prevent any changes to the element you are pointing to,
you write a definition like this:
const int* u;
Starting from the identifier, we read "u is a pointer, which points to
a const int." Here, no initialization is required because you're
saying that u can point to anything (that is, it is not const), but the
thing it points to cannot be changed.
Here's the mildly confusing part. You might think that to make the
pointer itself unchangeable, that is, to prevent any change to the
address contained inside u, you would simply move the const to
the other side of the int like this:
int const* v;
It's not all that crazy to think that this should read "v is a const
pointer to an int." However, the way it actually reads is "v is an
ordinary pointer to an int that happens to be const." That is, the
const has bound itself to the int again, and the effect is the same as
the previous definition. The fact that these two definitions are the
same is the confusing point; to prevent this confusion on the part of
your reader, you should probably stick to the first form.
8: Constants
361
img
const pointer
To make the pointer itself a const, you must place the const
specifier to the right of the *, like this:
int d = 1;
int* const w = &d;
Now it reads: "w is a pointer, which is const, that points to an int."
Because the pointer itself is now the const, the compiler requires
that it be given an initial value that will be unchanged for the life of
that pointer. It's OK, however, to change what that value points to
by saying
*w = 2;
You can also make a const pointer to a const object using either of
two legal forms:
int d = 1;
const int* const x = &d;  // (1)
int const* const x2 = &d; // (2)
Now neither the pointer nor the object can be changed.
Some people argue that the second form is more consistent because
the const is always placed to the right of what it modifies. You'll
have to decide which is clearer for your particular coding style.
Here are the above lines in a compileable file:
//: C08:ConstPointers.cpp
const int* u;
int const* v;
int d = 1;
int* const w = &d;
const int* const x = &d;  // (1)
int const* const x2 = &d; // (2)
int main() {} ///:~
362
Thinking in C++
img
Formatting
This book makes a point of only putting one pointer definition on a
line, and initializing each pointer at the point of definition
whenever possible. Because of this, the formatting style of
"attaching" the `*' to the data type is possible:
int* u = &i;
as if int* were a discrete type unto itself. This makes the code easier
to understand, but unfortunately that's not actually the way things
work. The `*' in fact binds to the identifier, not the type. It can be
placed anywhere between the type name and the identifier. So you
could do this:
int *u = &i, v = 0;
which creates an int* u, as before, and a non-pointer int v. Because
readers often find this confusing, it is best to follow the form shown
in this book.
Assignment and type checking
C++ is very particular about type checking, and this extends to
pointer assignments. You can assign the address of a non-const
object to a const pointer because you're simply promising not to
change something that is OK to change. However, you can't assign
the address of a const object to a non-const pointer because then
you're saying you might change the object via the pointer. Of
course, you can always use a cast to force such an assignment, but
this is bad programming practice because you are then breaking the
constness of the object, along with any safety promised by the
const. For example:
//: C08:PointerAssignment.cpp
int d = 1;
const int e = 2;
int* u = &d; // OK -- d not const
//! int* v = &e; // Illegal -- e const
int* w = (int*)&e; // Legal but bad practice
8: Constants
363
img
int main() {} ///:~
Although C++ helps prevent errors it does not protect you from
yourself if you want to break the safety mechanisms.
Character array literals
The place where strict constness is not enforced is with character
array literals. You can say
char* cp = "howdy";
and the compiler will accept it without complaint. This is
technically an error because a character array literal ("howdy" in
this case) is created by the compiler as a constant character array,
and the result of the quoted character array is its starting address in
memory. Modifying any of the characters in the array is a runtime
error, although not all compilers enforce this correctly.
So character array literals are actually constant character arrays. Of
course, the compiler lets you get away with treating them as non-
const because there's so much existing C code that relies on this.
However, if you try to change the values in a character array literal,
the behavior is undefined, although it will probably work on many
machines.
If you want to be able to modify the string, put it in an array:
char cp[] = "howdy";
Since compilers often don't enforce the difference you won't be
reminded to use this latter form and so the point becomes rather
subtle.
Function arguments
& return values
The use of const to specify function arguments and return values is
another place where the concept of constants can be confusing. If
364
Thinking in C++
img
you are passing objects by value, specifying const has no meaning to
the client (it means that the passed argument cannot be modified
inside the function). If you are returning an object of a user-defined
type by value as a const, it means the returned value cannot be
modified. If you are passing and returning addresses, const is a
promise that the destination of the address will not be changed.
Passing by const value
You can specify that function arguments are const when passing
them by value, such as
void f1(const int i) {
i++; // Illegal -- compile-time error
}
but what does this mean? You're making a promise that the
original value of the variable will not be changed by the function
f1( ). However, because the argument is passed by value, you
immediately make a copy of the original variable, so the promise to
the client is implicitly kept.
Inside the function, the const takes on meaning: the argument
cannot be changed. So it's really a tool for the creator of the
function, and not the caller.
To avoid confusion to the caller, you can make the argument a
const inside the function, rather than in the argument list. You
could do this with a pointer, but a nicer syntax is achieved with the
reference, a subject that will be fully developed in Chapter 11.
Briefly, a reference is like a constant pointer that is automatically
dereferenced, so it has the effect of being an alias to an object. To
create a reference, you use the & in the definition. So the non-
confusing function definition looks like this:
void f2(int ic) {
const int& i = ic;
i++;  // Illegal -- compile-time error
}
8: Constants
365
img
Again, you'll get an error message, but this time the constness of
the local object is not part of the function signature; it only has
meaning to the implementation of the function and therefore it's
hidden from the client.
Returning by const value
A similar truth holds for the return value. If you say that a
function's return value is const:
const int g();
you are promising that the original variable (inside the function
frame) will not be modified. And again, because you're returning it
by value, it's copied so the original value could never be modified
via the return value.
At first, this can make the specification of const seem meaningless.
You can see the apparent lack of effect of returning consts by value
in this example:
//: C08:Constval.cpp
// Returning consts by value
// has no meaning for built-in types
int f3() { return 1; }
const int f4() { return 1; }
int main() {
const int j = f3(); // Works fine
int k = f4(); // But this works fine too!
} ///:~
For built-in types, it doesn't matter whether you return by value as
a const, so you should avoid confusing the client programmer and
leave off the const when returning a built-in type by value.
Returning by value as a const becomes important when you're
dealing with user-defined types. If a function returns a class object
by value as a const, the return value of that function cannot be an
366
Thinking in C++
img
lvalue (that is, it cannot be assigned to or otherwise modified). For
example:
//: C08:ConstReturnValues.cpp
// Constant return by value
// Result cannot be used as an lvalue
class X {
int i;
public:
X(int ii = 0);
void modify();
};
X::X(int ii) { i = ii; }
void X::modify() { i++; }
X f5() {
return X();
}
const X f6() {
return X();
}
void f7(X& x) { // Pass by non-const reference
x.modify();
}
int main() {
f5() = X(1); // OK -- non-const return value
f5().modify(); // OK
// Causes compile-time errors:
//!  f7(f5());
//!  f6() = X(1);
//!  f6().modify();
//!  f7(f6());
} ///:~
f5( ) returns a non-const X object, while f6( ) returns a const X
object. Only the non-const return value can be used as an lvalue.
8: Constants
367
img
Thus, it's important to use const when returning an object by value
if you want to prevent its use as an lvalue.
The reason const has no meaning when you're returning a built-in
type by value is that the compiler already prevents it from being an
lvalue (because it's always a value, and not a variable). Only when
you're returning objects of user-defined types by value does it
become an issue.
The function f7( ) takes its argument as a non-const reference (an
additional way of handling addresses in C++ and the subject of
Chapter 11). This is effectively the same as taking a non-const
pointer; it's just that the syntax is different. The reason this won't
compile in C++ is because of the creation of a temporary.
Temporaries
Sometimes, during the evaluation of an expression, the compiler
must create temporary objects. These are objects like any other: they
require storage and they must be constructed and destroyed. The
difference is that you never see them ­ the compiler is responsible
for deciding that they're needed and the details of their existence.
But there is one thing about temporaries: they're automatically
const. Because you usually won't be able to get your hands on a
temporary object, telling it to do something that will change that
temporary is almost certainly a mistake because you won't be able
to use that information. By making all temporaries automatically
const, the compiler informs you when you make that mistake.
In the above example, f5( ) returns a non-const X object. But in the
expression:
f7(f5());
the compiler must manufacture a temporary object to hold the
return value of f5( ) so it can be passed to f7( ). This would be fine if
f7( ) took its argument by value; then the temporary would be
copied into f7( ) and it wouldn't matter what happened to the
368
Thinking in C++
img
temporary X. However, f7( ) takes its argument by reference, which
means in this example takes the address of the temporary X. Since
f7( ) doesn't take its argument by const reference, it has permission
to modify the temporary object. But the compiler knows that the
temporary will vanish as soon as the expression evaluation is
complete, and thus any modifications you make to the temporary X
will be lost. By making all temporary objects automatically const,
this situation causes a compile-time error so you don't get caught
by what would be a very difficult bug to find.
However, notice the expressions that are legal:
f5() = X(1);
f5().modify();
Although these pass muster for the compiler, they are actually
problematic. f5( ) returns an X object, and for the compiler to satisfy
the above expressions it must create a temporary to hold that
return value. So in both expressions the temporary object is being
modified, and as soon as the expression is over the temporary is
cleaned up. As a result, the modifications are lost so this code is
probably a bug ­ but the compiler doesn't tell you anything about
it. Expressions like these are simple enough for you to detect the
problem, but when things get more complex it's possible for a bug
to slip through these cracks.
The way the constness of class objects is preserved is shown later in
the chapter.
Passing and returning addresses
If you pass or return an address (either a pointer or a reference), it's
possible for the client programmer to take it and modify the
original value. If you make the pointer or reference a const, you
prevent this from happening, which may save you some grief. In
fact, whenever you're passing an address into a function, you
should make it a const if at all possible. If you don't, you're
8: Constants
369
img
excluding the possibility of using that function with anything that
is a const.
The choice of whether to return a pointer or reference to a const
depends on what you want to allow your client programmer to do
with it. Here's an example that demonstrates the use of const
pointers as function arguments and return values:
//: C08:ConstPointer.cpp
// Constant pointer arg/return
void t(int*) {}
void u(const int* cip) {
//!  *cip = 2; // Illegal -- modifies value
int i = *cip; // OK -- copies value
//!  int* ip2 = cip; // Illegal: non-const
}
const char* v() {
// Returns address of static character array:
return "result of function v()";
}
const int* const w() {
static int i;
return &i;
}
int main() {
int x = 0;
int* ip = &x;
const int* cip = &x;
t(ip);  // OK
//!  t(cip); // Not OK
u(ip);  // OK
u(cip); // Also OK
//!  char* cp = v(); // Not OK
const char* ccp = v(); // OK
//!  int* ip2 = w(); // Not OK
const int* const ccip = w(); // OK
const int* cip2 = w(); // OK
//!  *w() = 1; // Not OK
370
Thinking in C++
img
} ///:~
The function t( ) takes an ordinary non-const pointer as an
argument, and u( ) takes a const pointer. Inside u( ) you can see
that attempting to modify the destination of the const pointer is
illegal, but you can of course copy the information out into a non-
const variable. The compiler also prevents you from creating a non-
const pointer using the address stored inside a const pointer.
The functions v( ) and w( ) test return value semantics. v( ) returns
a const char* that is created from a character array literal. This
statement actually produces the address of the character array
literal, after the compiler creates it and stores it in the static storage
area. As mentioned earlier, this character array is technically a
constant, which is properly expressed by the return value of v( ).
The return value of w( ) requires that both the pointer and what it
points to must be const. As with v( ), the value returned by w( ) is
valid after the function returns only because it is static. You never
want to return pointers to local stack variables because they will be
invalid after the function returns and the stack is cleaned up.
(Another common pointer you might return is the address of
storage allocated on the heap, which is still valid after the function
returns.)
In main( ), the functions are tested with various arguments. You
can see that t( ) will accept a non-const pointer argument, but if you
try to pass it a pointer to a const, there's no promise that t( ) will
leave the pointer's destination alone, so the compiler gives you an
error message. u( ) takes a const pointer, so it will accept both types
of arguments. Thus, a function that takes a const pointer is more
general than one that does not.
As expected, the return value of v( ) can be assigned only to a
pointer to a const. You would also expect that the compiler refuses
to assign the return value of w( ) to a non-const pointer, and
accepts a const int* constbut it might be a bit surprising to see that
,
8: Constants
371
img
.
it also accepts a const int* which is not an exact match to the return
,
type. Once again, because the value (which is the address contained
in the pointer) is being copied, the promise that the original
variable is untouched is automatically kept. Thus, the second const
in const int* constis only meaningful when you try to use it as an
lvalue, in which case the compiler prevents you.
Standard argument passing
In C it's very common to pass by value, and when you want to pass
an address your only choice is to use a pointer1. However, neither
of these approaches is preferred in C++. Instead, your first choice
when passing an argument is to pass by reference, and by const
reference at that. To the client programmer, the syntax is identical
to that of passing by value, so there's no confusion about pointers ­
they don't even have to think about pointers. For the creator of the
function, passing an address is virtually always more efficient than
passing an entire class object, and if you pass by const reference it
means your function will not change the destination of that
address, so the effect from the client programmer's point of view is
exactly the same as pass-by-value (only more efficient).
Because of the syntax of references (it looks like pass-by-value to
the caller) it's possible to pass a temporary object to a function that
takes a const reference, whereas you can never pass a temporary
object to a function that takes a pointer ­ with a pointer, the address
must be explicitly taken. So passing by reference produces a new
situation that never occurs in C: a temporary, which is always
const, can have its address passed to a function. This is why, to
allow temporaries to be passed to functions by reference, the
argument must be a const reference. The following example
demonstrates this:
1 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.
372
Thinking in C++
img
//: C08:ConstTemporary.cpp
// Temporaries are const
class X {};
X f() { return X(); } // Return by value
void g1(X&) {} // Pass by non-const reference
void g2(const X&) {} // Pass by const reference
int main() {
// Error: const temporary created by f():
//!  g1(f());
// OK: g2 takes a const reference:
g2(f());
} ///:~
f( ) returns an object of class X by value. That means when you
immediately take the return value of f( ) and pass it to another
function as in the calls to g1( ) and g2( ), a temporary is created and
that temporary is const. Thus, the call in g1( ) is an error because
g1( ) doesn't take a const reference, but the call to g2( ) is OK.
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.
8: Constants
373
img
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
374
Thinking in C++
img
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;
8: Constants
375
img
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 << ' '; }
376
Thinking in C++
img
.
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 constwhen 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 pointers2:
//: C08:StringStack.cpp
// Using static const to create a
2 At the time of this writing, not all compilers supported this feature.
8: Constants
377
img
// 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 =
378
Thinking in C++
img
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 StringStackholds const string* If this
.
were not true, you couldn't use a StringStackto 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];
};
8: Constants
379
img
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.cppexample above, the line:
static const int size = 100;
would be instead:
enum { size = 100 };
Although you'll often see the enum technique in legacy code, the
static constfeature was added to the language to solve just this
problem. However, there is no overwhelming reason that you must
choose static constover 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);
380
Thinking in C++
img
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; }
8: Constants
381
img
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
382
Thinking in C++
img
}
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.
8: Constants
383
img
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++;
}
384
Thinking in C++
img
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.
8: Constants
385
img
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,
1.
The class or struct must have no user-defined constructors or
destructor.
2.
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 volatileis 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
volatiletells 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
386
Thinking in C++
img
optimizing the code to remove what would normally be a
redundant read.
You create volatileobjects using the same syntax that you use to
create const objects. You can also create const volatileobjects,
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() {
8: Constants
387
img
volatile Comm Port;
Port.isr(); // OK
//!  Port.read(0); // Error, read() not volatile
} ///:~
As with const, you can use volatilefor data members, member
functions, and objects themselves. You can only call volatile
member functions for volatileobjects.
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 volatileis 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 .
388
Thinking in C++
img
1.
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).
2.
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++.
3.
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.
4.
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.
5.
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.
6.
Create a const array of char, then try to change one of the
chars.
7.
Create an extern constdeclaration in one file, and put a
main( ) in that file that prints the value of the extern
const. Provide an extern constdefinition in a second file,
then compile and link the two files together.
8.
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.
9.
Write a const pointer to a double, and point it at an array
of double. Show that you can change what the pointer
8: Constants
389
img
points to, but you can't increment or decrement the
pointer.
10.
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.
11.
Remove the comment on the error-generating line of
code in PointerAssignment.cpp see the error that your
to
compiler generates.
12.
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?
13.
Create a function that takes an argument by value as a
const; then try to change that argument in the function
body.
14.
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.
15.
Modify ConstReturnValues.cpp
removing comments on
the error-causing lines one at a time, to see what error
messages your compiler generates.
16.
Modify ConstPointer.cpp
removing comments on the
error-causing lines one at a time, to see what error
messages your compiler generates.
17.
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).
18.
Modify ConstTemporary.cpp
removing the comment on
the error-causing line to see what error messages your
compiler generates.
19.
Create a class containing both a const and a non-const
float. Initialize these using the constructor initializer list.
390
Thinking in C++
img
20.
Create a class called MyStringwhich contains a string
and has a constructor that initializes the string, and a
print( )function. Modify StringStack.cppso that the
container holds MyStringobjects, and main( ) so it prints
them.
21.
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.
22.
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.
23.
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.
24.
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.
25.
In Mutable.cpp remove the comment on the error-
,
causing line to see what sort of error message your
compiler produces.
26.
Modify Quoter.cppby making quote( )a const member
function and lastquotemutable.
27.
Create a class with a volatiledata member. Create both
volatileand non-volatilemember functions that modify
the volatiledata member, and see what the compiler
says. Create both volatileand non-volatileobjects of
your class and try calling both the volatileand non-
volatilemember functions to see what is successful and
what kind of error messages the compiler produces.
28.
Create a class called bird that can fly( ) and a class rock
that can't. Create a rock object, take its address, and
8: Constants
391
img
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++?
392
Thinking in C++