Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 15.  Values


15.5. Constants

15.5.1 Simple Constants

Constants in C++ take the following straightforward form:



const long  SOME_CONST  = 10;



The value of SOME_CONST is fixed, thus SOME_CONST can be used wherever an integer literal may be used, such as in specifying the dimensions of arrays, in enum values, and in the integer parameters of templates.



int ai[SOME_CONST];                             // ok


enum { x = SOME_CONST };                        // ok


frame_array_d2<int, SOME_CONST, SOME_CONST> ar; // ok



All fundamental types can be used in the definition of constants, although floating-point constants can only be used where floating-point literals can, for example, they cannot be used to define the bounds of arrays. The compiler assigns the values of such constants at compile time and, except where their addresses are taken, the constants occupy no storage in the linked module.

That's all straightforward and pretty obvious. Where the problems come in is when engineers get a bit slap happy with casts. Consider the following code:



const int W = 10;





int main()


{


  int &w = const_cast<int&>(W);


  int const   *p = &W;


  w *= 2;


  int const   *q = &W;


  int i[W];


  printf("%d, %d, %d, %d, %d\n", W, NUM_ELEMENTS(i), w, *p, *q);


  return 0;


}



It's not a reasonable thing to do, and the standard (C++-98: 5.2.11,7 & 7.1.5.1,7) tells us that such behavior is undefined. Hence, there is the expected disagreement between the compilers. Digital Mars gives 10,10,20,10,10. Borland, CodeWarrior, Intel, and Watcom give 10,10,20,20,20. Arguably the most useful behavior, from the perspective of the maintainer at least, is that of GCC and Visual C++ (from version 4.2 all the way up to 7.1), which both crash on the assignment to w.

Constraint: Casting away constness of a constant object is undefined, and will likely lead to unintended consequences.


The "answer" to this is simple: don't cast away the constness of anything that you don't know to be actually non-const, otherwise chaos will rain down upon you!

15.5.2 Class-Type Constants

It is possible to define constants of class type, as in



class Z


{


public:


  Z(int i)


    : z(i)


  {}


public:


  int z;


};





const Z g_z = 3;



However, in such cases the constant instance may not be used in place of integer literals.



int ai[g_z.z];                       // error


enum { x = g_z.z };                  // error





frame_array_d2<int, g_z.z, g_z.z);   // error



Furthermore, the constant objects are not created at compile time rather at run time.[9] Since they have static scope (i.e., they exist outside the scope of any particular function), they must be constructed (and destroyed) during program execution. You need to be aware that there are three potential drawbacks to their use in this regard:

[9] That's not to say that some cannot be (and are not) elided in the optimization steps in compiling and/or linking.

First, the constructors and destructor for the particular type may have significant run time cost. This is borne at application/module start-up and shutdown, where such costs are usually, though not always, of concern.

Second, there is the potential for one global object to depend on another. Consider the following (admittedly contrived) situation:



const Z g_z = 3;





extern const Z g_z3;





const Z g_z2 = g_z3;


const Z g_z3 = g_z;





int main()


{


  printf("%d %d %d\n", g_z, g_z2, g_z3);


  . . .



I trust you realize that it's not going to print 3, 3, 3. You actually get 3, 0, 3. The reason is that g_z2 is copied from g_z3 before g_z3 is initialized. Since global objects reside in global memory, which is guaranteed to be 0-initialized (see chapter 11), all the member of g_z3 are, preconstruction, 0, whence the value of g_z2. Rare, but nasty.

Third, if (non-extern) class-type constants are shared (i.e., by being declared in a shared header) between compilation units, then each compilation unit actually receives a separate copy of the constant instance. Obviously, if the definition or use of such constants relies on a singleton nature, it is broken.

Constraint: Constants of class type are evaluated as immutable global objects, rather than as compile-time constants. Failure to be mindful of this distinction is hazardous.


The simple answer to these hazards is to avoid using such constants if possible, or to proceed with caution[10] if not.

[10] Some companies actually have strict policies regarding the use of statics in this vein, especially when a value is to be changed.

15.5.3 Member Constants

As well as declaring constants at global and function scope, they may also be declared within the scope of classes, as in the following:



class Y


{


public:


  static const int  y = 5;  // member constant


  static const int  z;      // constant static member


};



These member constants are declared with very similar syntax to (const) static members, except that they have the form of a variable declaration and initialization statement, and they are limited to integral or enum (C++-98: 9.4.2,4) type. By using the initialization syntax, you declare that the compiler is free to interpret the member as a literal constant. In this case they differ from static members because you can use them in compile-time expressions, and you do not have to separately define them.

Well, actually, that last part is only sort of true. The same paragraph (C++-98: 9.4.2 in the standard) says, "The [static] member shall still be defined . . . if it is used in the program." This implies that we do indeed need to provide a definition, just as we would for any other static member. However, this is theory, and we'll see that in practice (see Table 15.1) none of our compilers require the definition in order to make use of the constant in a linked and functioning program. Nonetheless, if you take the address of a member constant, or make a reference to it, it will need to be given a definition:



int const *p = &Y::y;


int const &r = Y::y;





/* static */ const int   Y::y; // Define here



Table 15.1. Support for integral and floating-point member constants.

Compiler

Supports Member Constants

Integral

Requires Definition

Floating Point

Borland 5.6.4

Yes

No

No

CodeWarrior 8

Yes

No

No

Comeau 4.3.3

Yes

No

No

Digital Mars 8.40

Yes

No

No

GCC 2.95

Yes

No

Yes

GCC 3.2

Yes

No

Yes

Intel 8

Yes

No

No

Visual C++ 6

No

-

No

Visual C++ 7.1

Yes

No

No

Open Watcom 1.2

No

-

No


Despite the need for the definition, you do not initialize here, as the compiler does that for you. This helps to avoid different definitions of the same constant in different link units. Naturally, if your application code sees the constant with one value, and your dynamic library sees another, things are not likely to run swimmingly for very long. However, this modicum of safety can easily be subverted (deliberately or accidentally) since dynamic libraries are usually loadable without rebuilding. Still, it's better than nothing, and if your development procedures are up to scratch, you should avoid such clashes until you get into deployment.

While member constants are a nice feature, there are a couple of imperfections. First, not all compilers currently support them, as shown in Table 15.1.

There is a pretty straightforward workaround for this, which suffices for most cases: use enums. The downside is that this may not work for integral types that are larger than the range of enum, since the standard provides a very open definition of how much space an enum occupies. It says the "underlying type shall not be larger than an int unless the value of an enumerator cannot fit in an int or unsigned int." Note that it does not say that the type will be larger, and in practice most compilers do not allow enumerators to have values larger than their int type. The following enumeration provokes a wide range of behavior in our compilers:



enum Big


{


  big = 0x1234567812345678


};



Borland, CodeWarrior, Comeau, GCC (2.95), and Intel all refuse to compile it, claiming it's too large, and they have a perfect right to do so. Digital Mars, GCC (3.2), and Open Watcom all compile it and are able to print out its correct value (in decimal): 1311768465173141112. Visual C++ (6 and 7.1) compiles and prints out a value, but we only get the correct value if big is first assigned to an integer and we use printf() rather than the IOStreams. I think the answer regarding defining enum values larger than int is simple: don't.

Recommendation: Avoid using enum values larger than those that will fit in an int.


Notwithstanding this aspect, using enum for contants this is a very well-used technique, and quite successful where applicable. Indeed, I would say that it should be the preferred approach for anyone who has to maintain even a minimal degree of backward compatibility. (Note that the Boost libraries use macros to select between one option and the other, which is perfectly reasonable. You may be like me[11] though and prefer to dodge macros wherever possible.)

[11] MFC battle-hardened, that is.

The second issue is that member constants cannot be floating point type.



class Maths


{


public:


  static const double pi = 3.141592653590; // Member constants must be integral


};



This is a strange inconsistency given that they are valid as members of a namespace.



namespace Maths


{


  const double pi = 3.141592653590; // Ok


}



I must confess I don't really know why member constants cannot be floating point, when it is legal to define nonmember constants of floating point types. [Vand2003] states that there is no serious technical impediment to it, and some compilers do support them (see Table 15.1). It may be because a floating-point variable of a given size, on a given platform, could vary in its precise value between compilers, which is not the case for integers. But then, nonmember constants are subject to this same potential behavioral inaccuracy, so it seems an arbitrary choice. It may also have been influenced by the fact that floating-point literal constants may not take part in compile-time evaluation of constant expressions—such as defining the extent of an array dimension—that integral constants can, and therefore it was deemed less important to support them. However both these reasons are subject to the counter that nonmember constants have the same issues, so it is inconsistent.

Imperfection: C++ does not support member constants of floating-point type.


If your compiler does not support member constants and the values you wish to use are too large for enum, or your constant needs to be of floating-point type, you can use an alternative mechanism based on static methods. This is simply achieved by use of static methods, as in:



class Maths


{


public:


  static const double pi()


  {


    return 3.141592653590;


  }


};



Thus calling Maths::pi() provides access to the constant value. This is the approach used in the numeric_limits<> TRaits type in the standard library. Note, however, that this is a function call; it is likely that your compiler will optimize out all instances of the actual call, but it is still the case that the value is evaluated at run time and cannot participate in compile-time expressions, as in:



class X


{


  static const int i1 = std::numeric_limits<char>::digits; // Ok


  static const int i2 = std::numeric_limits<char>::max();  // Error


};



The member constant digits may be used, because it is itself a compile-time constant, but the result of max() may not because it is a function, even though it may well be optimized out because it always returns the same constant value.

15.5.4 Class-Type Member Constants

We saw with all constants of fundamental type that they are true constants in the sense of being evaluated at compile time, but that if you wish to take the address of such constants they must have a single, separate definition to provide their storage. We saw with class-type constants that they must be initialized at run time, which means there is a potential for using them before they're initialized (or after they're uninitialized), and also that they may not participate in compile-time expressions. This latter restriction also applies to static member functions that aim to provide member constants of types not supported by the language.

These limitations come together when we look at class-type member constants, which are not supported by the language, and largely for good reason, as we'll see.



class Rectangle


{


public:


  static const String  category("Rectangle"); // Not allowed


};



Constraint: C++ does not support member constants of class type.[12]


[12] Well, in a sense it does, insofar as a member of const type can be declared in a class, and defined elsewhere. However, this is not the same as a constant. Anyway, this hair-splitting is ruining my point.

There are two reasons for this: identity and timing. With normal static class members, the member is declared in the class declaration, and the storage is defined elsewhere, usually in an accompanying implementation file, as in:



// In Rectangle.h


class Rectangle


{


public:


  static const String  category;


};





// In Rectangle.cpp


/* static */ const String Rectangle::category("Rectangle");



As we saw in section 9.2, within a given link unit (executable or dynamic library), there is a single definition, residing in Rectangle.cpp. This instance is constructed by the language run time support, when the link unit is loaded into a running state (i.e., when the process starts executing or when the dynamic library is loaded into the process). It is destroyed when the link unit is unloaded from its running state. Thus there is a single entity, and the timing of it is defined.

Having said that, though, the real world can come along and bite us. In multimodule development there are several problems with timing and identity, for example, instances can be brought into a process in more than one dynamic library link unit (see section 9.2.3). It should be clear that if the member constant form was allowed for class types then the compiler would have to make even more decisions on our behalf than it already does. For example, the compiler/linker would have to work together outside of the purview of the developer in order to provide an implicit definition of the static instance. Which compilation unit would it insert it into? How would that affect potential ordering issues?

If you want the whole of the class in a header file and/or you want the definition visible within the class declaration, you could provide the constant via a static method, as in:



class Rectangle


{


public:


  static const String  category()


  {


    static const String   s_category("Rectangle");


    return s_category;


  }


};



The static method category() uses a local static instance of String, s_category, which is initialized once the first time that category() is called, and that instance is then returned to all subsequent callers of the method. A by-product of this is that s_category is only initialized when needed; if category() is never called, then the initialization cost is not incurred. This is known as lazy evaluation [Meye1996].

This is perfectly fine if the class is only ever used in single-threaded processes, but, as we saw in section 11.3, it is not thread safe in its current guise, and therefore must not be used in multi threaded environments, or in any library that has an outside chance of being used in such environments in the future. In most nontrivial applications, you can be better off reading such things, for example, a String, from configuration information (file, Web-based token, registry key) associated with the application.

The problem is that one thread could have just in-place constructed s_category but not yet have set the hidden flag variable. At this point a second thread (or several) may come in and carry out the in-place construction of s_category again. The least amount of badness that would happen in such a circumstance is that whatever resources the first construction would have allocated would be lost. Of course, two threads could both be partway through the construction, and the inconsistent state may very well lead to a crash.

The really nasty part about this issue is that it will so very rarely cause a problem, and even when it does it may be a benign effect. That's because once any thread completes step 2, any subsequent thread entering the function for the entire duration of the process will have well-defined behavior. You could run millions of instances of a process containing such code,[13] and never find this, but there still remains the possibility that the program will behave correctly, that is, according to your "design," and yet still crash.

[13] Perhaps running a Web server, a heart monitor, a nuclear power station!

We could use some of the magic of spin mutexes (see section 10.2.2) to efficiently render function-local static objects safe, much as we did in section 11.3.2, but that is only half the problem. It still leaves the "constant" with an identity crisis. If it is an entity type (see section 4.2) rather than a value type (see section 4.6), we're still in the soup. So I'll just say once more: never use this technique!

Recommendation: Never attempt to simulate class-type member constants with function local static objects.



      Previous section   Next section