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:
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
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:
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 introduced by the preprocessor, 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:
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” and that 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]; // IllegalIn
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 bufsize = 100;
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
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 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:
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:
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.