
Programming Pointers: Static vs. Dynamic Initialization
by Dan Saks
Last
month, I explained how the C standard grants compilers license
to place data into ROM. (See Placing Data into ROM with Standard
C, November 1998, p. 111.) This month, Ill look at some reasons
why the C++ standard has a harder time granting a similar license to
C++
compilers. Whereas the C standard has at least a few explicit statements
about read-only memory, the C++ standard has none. In fact, the terms
read-only memory and ROM never appear in the C++
standard, not even in a footnote. However, if you read the standard
carefully, you can reasonably deduce that C++ compilers have about as much
latitude to place objects into ROM as C compilers do. But you have to read
very
carefully.
The freedom that C compilers have to place objects
in ROM stems from one fairly simple rule appearing in a footnote:
- The implementation may place a const object that is not volatile in a
read-only region of storage
As I explained last month, these words apparently allow for programs that
can write to random-access memory (RAM) and then write-protect that RAM at
run time. In my experience, very few, if any, embedded systems employ
write-protected RAM as ROM. Therefore, Ill use the term ROM to mean
memory that the program can
never
write to.
If the
program can never write to ROM, the compiler can place an object into ROM
only if it can determine the objects location and initial contents
during compilation and linking. The compiler can do this for objects with
static storage, but not for objects with automatic or dynamic storage.
Thus, using my more restrictive definition for ROM, C compilers have the
following permission:
- An implementation may place an object into ROM if that object is const
and
non-volatile, and has static storage
My interpretation of the C++ standard is that it grants compilers similar
permission to place objects into ROM. However, while the requirements
listed abovethat the object must be const, non-volatile, and have
static storageare just as necessary in C++, they are not sufficient.
The conditions for placing an object in ROM must account for the added
capabilities of C++.
Non-constant initializer expressions
In both C and C++, the optional
initializer in a declaration can be an
expression rather than just a single value. For example:
int k = j + 1;
initializes k with the value of expression
j + 1
. If
j
is
a constant, then
j + 1
is a
constant-expression
, which the
compiler can evaluate during compilation.
The initializer for an object of array or structure type can be an
initializer-list
. That is, it can be a sequence of comma-separated
expressions enclosed in braces. For example:
struct rational
{
int num, den;
};
...
rational r = { m + 1, 2 * n + 1 };
initializes
r.num
with the value of
m + 1
and
r.den
with the value of
2 * n + 1
. Again, if
m
is constant, then
m + 1
is constant, and if n is constant, then
2 * n + 1
is also.
If the previous declaration for
k
appears in block scope (in a
function body), then
k
has automatic storage. If
k
s
declaration appears in global scope
or (in the case of C++) in namespace
scope, then
k
has static storage. You can also ensure that
k
has static storage by adding an extern or static specifier to
the declaration. The same is true for
r
.
This brings us to a point where C++ differs from C. In C, all the
expressions in the initializer for an object with static storage must be
constant expressions. In C++, they can be non-constant expressions. For
example, if you compile:
int f(int j)
{
static int k = j +
1;
...
}
as C, the compiler will reject ks declaration because the initializer
j + 1 is not a constant expression. If you compile it as C++, the compiler
will accept it and generate a program that initializes k using the value of
j on the first call to function f.
Now consider:
int f(int j)
{
static int const k = j + 1;
...
}
As I explained earlier, a C compiler can place an object into ROM if that
object is const, non-volatile, and has static storage. If these
requirements were also sufficient for C++, then a C++ compiler could place
k into ROM. But clearly, it cant. Because j + 1 is not constant, the
compiler cant initialize k until run time.
Constructors and destructors
Based on the discussion so far, it appears that a C++ compiler may place an
object into ROM if that object is const and non-volatile, has static
storage,
and all the expressions in its initializer (if present) are
constant expressions
. However, this rule is still too
narrow because it
doesnt account for objects of class types with constructors and
destructors.
Consider these C++ declarations:
class T
{
public:
T();
T(int i);
...
};
...
static T const x = 1;
Here, x is an object thats const and non-volatile with static storage
and only constant expressions in its initializer, yet the compiler might
not be able to place it into ROM.
Why not? Object x has type T, and T is a class type with a user-defined
constructor and destructor.
Constructors initialize objects, usually at run
time. In the declaration for x just above, 1 is the argument to the
constructor T(int). The declaration behaves pretty much as if it were
written as:
static T const x(1);
Notice that I didnt say the compiler cant place x into ROM; I
said it
might
not be able to. I believe the C++ standard lets
compilers place an object of a type with a constructor into ROM if the
compiler can figure a way to initialize the object at compile time. The
compiler can do this only if the objects initialization meets certain
constraints. Those constraints are too hairy to cover at this time.
Im not even sure I understand them yet.
Destructors that release resources at run time have a similarly chilling
effect on the compilers ability to place objects into ROM.
Once more, from the top
Lets recap. A compiler, whether for C or C++, can place an object in
ROM only if that object is const, non-volatile, and has static
storage.
Furthermore, the compiler must be able to initialize the object
statically
(at compile time) rather than
dynamically
(at run
time).
In C, its fairly easy to tell when youre using static
initialization. C has no class types with constructors that perform dynamic
initialization. In C, all the expressions in the initializer for an object
with static storage must be constant expressions (which can be evaluated at
compile time). Thus, the only way that C will initialize a const,
non-volatile object in static storage is by static initialization.
In
contrast, C++ supports dynamic as well as static initialization for objects
with static storage. A declaration might use dynamic initialization if the
initializer contains one or more non-constant expressions. A declaration
might also use dynamic initialization if it invokes a constructor.
The C
standard grants compilers some freedom to transform dynamic initializations
into static initializations. However it can be hard to tell if a
particular
C++ declaration that might use static initialization will actually do so.
Thus it can be hard to tell if an object declared const, non-volatile, and
with static storage will actually wind up in ROM.
Fortunately, the C++
standard has rules that dictate when a declaration
must
use static
initialization. These rules dont include all cases where a
declaration
might
use static initialization, but and they can give
you a pretty good idea of when you can reasonably expect a C++
compiler to
place objects into ROM, and theyre not too complicated. The rules
will be the subject of my next column.
Dan Saks is the president of Saks & Associates, and a contributing
editor for the
C/C++ Users Journal
. You can write to him at
dsaks@wittenberg.edu.
Return to Embedded.com
Send comments to:
Webmaster
All material on this site Copyright © 2000
CMP Media Inc. All rights reserved.
|