Inapoi
Inainte
Cuprins
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.
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 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:
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 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.
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
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 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. 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.
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() {} ///:~
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
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.
 |
|