Design Con 2015

Enumeration Constants vs. Constant Objects

November 30, 2001

Dan_Saks-November 30, 2001

Enumeration Constants vs. Constant Objects

Choosing between symbolic constants as either enumeration constants or constant objects is a close call. Here are some insights to break the tie.

C++ offers three distinct ways to define symbolic constants: as macros, enumeration constants, or constant objects. For example, if you would like to define buffer_size as a symbol representing 256, you can define it as a macro:

#define buffer_size 256

as an enumeration constant:

enum { buffer_size = 256 };

or as a constant object:

int const buffer_size = 256;

No matter which you choose, you can use buffer_size anywhere that you could use a numeric literal, as in:

char buffer[buffer_size];

As I explained last month, you should avoid using macros. (See "Symbolic Constants," November 2001, p. 55.) First, macros don't observe the usual scope rules. If you declare a macro in a local scope, don't expect it to stay there. Second, some compilers don't preserve macro names for use by symbolic debuggers.

Avoiding macros leaves you with a choice between constant objects and enumeration constants. Constant objects have a couple of advantages over enumeration constants. First, a constant object definition looks more like what it is. For example, when you write:

int const buffer_size = 256;

the definition says fairly explicitly that buffer_size is an "integer constant" whose value is 256. It's not so clear that:

enum { buffer_size = 256 };

is essentially the same thing.

Second, a constant object definition lets you specify the exact type of the constant. For example:

unsigned int const buffer_size = 256;

defines buffer_size as a constant whose type is unsigned int rather than plain int. You don't have a choice for the type of an enumeration constant. An enumeration constant whose value can be represented as an int is always an int.

Despite the advantages of constant objects, I generally prefer defining symbolic constants as enumeration constants. The problem with constant objects is that they pose a small risk of making your programs a little bigger or slower than they would be if you used enumeration constants. Even though the risk is small, it's almost always unnecessary.

Non-modifiable lvalues vs. rvalues

The crux of the problem with constant objects is that, while an enumeration constant is an rvalue, a constant object is a non-modifiable lvalue. (See "Non-modifiable Lvalues," July 2001, p. 56.) Consequently, a C++ compiler has more latitude to generate storage for constant objects than it does for enumeration constants. On occasion, a compiler might generate storage for a constant object, even though the program doesn't really need that storage. It might even generate additional code to initialize the constant at run time.

Consider this definition:

enum { N = 100 };

which defines N as an enumeration constant. In this case, N is an rvalue, which means you can't assign to it and you can't take its address. For example:

int *p = &N;        // error

is an error because N is not addressable. Declaring p as a "pointer to const" doesn't change the situation:

int const *p = &N;    // error

No matter how you try, you can't take the address of an enumeration constant.

On the other hand:

int const N = 100;

defines N as a constant object. In this case, N is a non-modifiable lvalue. You still can't assign to it, but you can take its address. For example:

int const *p = &N;    // ok

Here the const does make a difference. If you leave it out, as in:

int *p = &N;// error

the pointer declaration no longer compiles.

Although a program can take the address of a constant object, it's rarely done. If a C++ compiler can determine that the program never needs storage for a particular constant object, it need not allocate anything for that object. Most modern C++ compilers are good at implementing this optimization. Nonetheless, I have seen compilers generate unnecessary storage and executable code for constant objects.

Global constant objects

In both C and C++, an object declared at the global scope has static storage duration. This means that if the compiler allocates storage for the object, it will be in a statically allocated data segment, rather than on the stack or in a dynamic memory pool. Thus, if you define:

int const N = 100;

at global scope and the compiler does generate storage for N, that storage will be in an initialized static data segment. On a traditional desktop programming platform, that storage will be initialized as the program loads into memory. In a typical embedded system, that storage will likely be in ROM. In either case, initializing N requires no program execution time.

In C++, a constant object declared at the global scope has internal linkage by default. It behaves as if it had been declared with the keyword static, as in:

static int const N = 100;

In this example, it means that only code appearing in the same translation unit as the definition for N can refer to this particular N. There may be references to N in other translation units, but they refer to different Ns.A C++ compiler has the freedom to eliminate the storage allocated for a constant object if it can determine that the program never needs that storage. For example, if you define:

int const N = 100;

and later store its address into a pointer, the compiler must generate storage for N so the pointer has something to point to. But if all you do is use N as an array dimension, as in:

int a[N];

or as a case label, as in:

switch (v)
    {
    case N:
        ...
    }

then the compiler need not generate storage for N.

Since a constant object has internal linkage by default, the compiler need not analyze the entire program (something it can't do anyway) to decide whether to allocate storage for the object. The compiler needs to analyze only the translation unit containing the constant object's definition. If the code in the translation unit never takes the address of or binds a reference to the constant object, the compiler can avoid allocating storage.

In C, constant objects-in fact, all objects-declared at global scope have external linkage by default. That is, they behave as if they had been declared with the keyword extern, as in:

extern int const N = 100;

In this example, it means that references to this N may appear in translation units in addition to the one containing N's definition. Thus, a C compiler can't avoid generating storage for a constant object based on the analysis of a single translation unit. It must generate storage for a constant object with external linkage in case some other translation unit refers to the object. It falls on the linker to eliminate the storage if it can. My experience is that most can't.

Local constant objects

An object declared inside a function body normally has automatic storage duration. Thus, if the compiler allocates storage for the object, it will be on the stack. For example, if you define:

void f()
    {
    int const N = 100;
    ...

the compiler might transform this into code that allocates storage and initializes N every time the program calls f.

A good optimizing compiler might eliminate the storage allocation and initialization, and treat N as a compile-time constant. I suggest you compile some test cases with your compiler and examine the generated code.

If your compiler does generate code to initialize local integer constants at run time, you can usually get the compiler to move the allocation and initialization to translation time by declaring N with the keyword static, as in:

void f()
    {
    static int const N = 100;
    ...


This isn't difficult, but it is something you must remember to do.

Enumeration constants

The code generation issues that I described for constant objects are not a concern with enumeration constants. Enumeration constants are rvalues. As such, they have neither storage duration nor linkage. You can't declare an enumeration constant as static or extern. Although you can declare an enumeration constant in local or global scope, the declaration's scope is unlikely to affect the way the compiler generates code for the constant.

As I mentioned earlier, an advantage of constant objects over enumeration constants is that you can take the address of a constant object. On the other hand, the ability to take the address of a constant object sometimes leads compilers to generate unnecessary code, so maybe it's not much of an advantage after all.

I can't recall the last time I wrote code in which I took the address of a symbolic integer constant. I can recall the last time I cared about my code being compact and efficient. Since compilers are less likely to generate run-time storage and initialization code for enumeration constants than for constant objects, I'll stick with enumeration constants.

Dan Saks is the president of Saks & Associates, a C/C++ training and consulting company. He is also a contributing editor of the C/C++ User's Journal. You can write to him at dsaks@wittenberg.edu.

Return to December 2001 Table of Contents

Loading comments...

Most Commented

Parts Search Datasheets.com

KNOWLEDGE CENTER