Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Classes

This section shows the ways you can use const with classes. You may want to create a local const in a class to use inside constant expressions that will be evaluated at compile time. However, the meaning of const is different inside classes, so you must understand the options in order to create const data members of a class.

You can also make an entire object const (and as you’ve just seen, the compiler always makes temporary objects const). But preserving the constness of an object is more complex. The compiler can ensure the constness of a built-in type but it cannot monitor the intricacies of a class. To guarantee the constness of a class object, the const member function is introduced: only a const member function may be called for a const object.

const in classes

One of the places you’d like to use a const for constant expressions is inside classes. The typical example is when you’re creating an array inside a class and you want to use a const instead of a #define to establish the array size and to use in calculations involving the array. The array size is something you’d like to keep hidden inside the class, so if you used a name like size, for example, you could use that name in another class without a clash. The preprocessor treats all #defines as global from the point they are defined, so this will not achieve the desired effect.

You might assume that the logical choice is to place a const inside the class. This doesn’t produce the desired result. Inside a class, const partially reverts to its meaning in C. It allocates storage within each object and represents a value that is initialized once and then cannot change. The use of const inside a class means “This is constant for the lifetime of the object.” However, each different object may contain a different value for that constant.

Thus, when you create an ordinary (non- static) const inside a class, you cannot give it an initial value. This initialization must occur in the constructor, of course, but in a special place in the constructor. Because a const must be initialized at the point it is created, inside the main body of the constructor the const must already be initialized. Otherwise you’re left with the choice of waiting until some point later in the constructor body, which means the const would be un-initialized for a while. Also, there would be nothing to keep you from changing the value of the const at various places in the constructor body.

The constructor initializer list

The special initialization point is called the constructor initializer list, and it was originally developed for use in inheritance (an object-oriented subject of a later chapter). The constructor initializer list – which, as the name implies, occurs only in the definition of the constructor – is a list of “constructor calls” that occur after the function argument list and a colon, but before the opening brace of the constructor body. This is to remind you that the initialization in the list occurs before any of the main constructor code is executed. This is the place to put all const initializations. The proper form for const inside a class is shown here:

//: C08:ConstInitialization.cpp
// Initializing const in classes
#include <iostream>
using namespace std;

class Fred {
  const int size;
public:
  Fred(int sz);
  void print();
};

Fred::Fred(int sz) : size(sz) {}
void Fred::print() { cout << size << endl; }

int main() {
  Fred a(1), b(2), c(3);
  a.print(), b.print(), c.print();
} ///:~

The form of the constructor initializer list shown above is confusing at first because you’re not used to seeing a built-in type treated as if it has a constructor.

“Constructors” for built-in types

As the language developed and more effort was put into making user-defined types look like built-in types, it became apparent that there were times when it was helpful to make built-in types look like user-defined types. In the constructor initializer list, you can treat a built-in type as if it has a constructor, like this:

//: C08:BuiltInTypeConstructors.cpp
#include <iostream>
using namespace std;

class B {
  int i;
public:
  B(int ii);
  void print();
};

B::B(int ii) : i(ii) {}
void B::print() { cout << i << endl; }

int main() {
  B a(1), b(2);
  float pi(3.14159);
  a.print(); b.print();
  cout << pi << endl;
} ///:~

This is especially critical when initializing const data members because they must be initialized before the function body is entered.

It made sense to extend this “constructor” for built-in types (which simply means assignment) to the general case, which is why the float pi(3.14159) definition works in the above code.

It’s often useful to encapsulate a built-in type inside a class to guarantee initialization with the constructor. For example, here’s an Integer class:

//: C08:EncapsulatingTypes.cpp
#include <iostream>
using namespace std;

class Integer {
  int i;
public:
  Integer(int ii = 0);
  void print();
};

Integer::Integer(int ii) : i(ii) {}
void Integer::print() { cout << i << ' '; }

int main() {
  Integer i[100];
  for(int j = 0; j < 100; j++)
    i[j].print();
} ///:~

The array of Integers in main( ) are all automatically initialized to zero. This initialization isn’t necessarily more costly than a for loop or memset( ). Many compilers easily optimize this to a very fast process.

Compile-time constants in classes

The above use of const is interesting and probably useful in cases, but it does not solve the original problem which is: “how do you make a compile-time constant inside a class?” The answer requires the use of an additional keyword which will not be fully introduced until Chapter XX: static. The static keyword, in this situation, means “there’s only one instance, regardless of how many objects of the class are created,” which is precisely what we need here: a member of a class which is constant, and which cannot change from one object of the class to another. Thus, a static const of a built-in type can be treated as a compile-time constant.

There is one feature of static const when used inside classes which is a bit unusual: you must provide the initializer at the point of definition of the static const . This is something that only occurs with the static const ; as much as you might like to use it in other situations it won’t work because all other data members must be initialized in the constructor or in other member functions.

Here’s an example that shows the creation and use of a static const called size inside a class that represents a stack of string pointers:

//: C08:StringStack.cpp
// Using static const to create a compile-time
// constant inside a class
#include <string>
#include <iostream>
using namespace std;

class StringStack {
  static const int size = 100;
  const string* stack[size];
  int index;
public:
   StringStack();
   void push(const string* s);
   const string* pop();
};

StringStack::StringStack() : index(0) {
  memset(stack, 0, size * sizeof(string*));
}

void StringStack::push(const string* s) {
  if(index < size)
    stack[index++] = s;
}

const string* StringStack::pop() {
  if(index > 0) {
    const string* rv = stack[--index];
    stack[index] = 0;
    return rv;
  }
  return 0;
}

string iceCream[] = {
  "pralines & cream",
  "fudge ripple",
  "jamocha almond fudge",
  "wild mountain blackberry",
  "raspberry sorbet",
  "lemon swirl",
  "rocky road",
  "deep chocolate fudge"
};

const int iCsz = 
  sizeof iceCream / sizeof *iceCream;

int main() {
  StringStack ss;
  for(int i = 0; i < iCsz; i++)
    ss.push(&iceCream[i]);
  const string* cp;
  while((cp = ss.pop()) != 0)
    cout << *cp << endl;
} ///:~

Since size is used to determine the size of the array stack, it is indeed a compile-time constant, but one that is hidden inside the class.

Notice that push( ) takes a const string* as an argument, pop( ) returns a const string*, and StringStack holds const string* . If this were not true, you couldn’t use a StringStack to hold the pointers in iceCream. However, it also prevents you from doing anything that will change the objects contained by StringStack. Of course, not all containers are designed with this restriction.

The “enum hack” in old code

In older versions of C++, static const was not supported inside classes. This meant that const was useless for constant expressions inside classes. However, people still wanted to do this so a common solution (typically referred to as the “enum hack”) was to use an untagged enum with no instances. An enumeration must have all its values established at compile time, it’s local to the class, and its values are available for constant expressions. Thus, you will commonly see (in older code [32]):

//: C08:EnumHack.cpp
#include <iostream>
using namespace std;

class Bunch {
  enum { size = 1000 };
  int i[size];
};

int main() {
  cout << "sizeof(Bunch) = " << sizeof(Bunch) <<
    ", sizeof(i[1000]) = " << sizeof(int[1000])
    << endl;
} ///:~

The use of enum here is guaranteed to occupy no storage in the object, and the enumerators are all evaluated at compile time. You can also explicitly establish the values of the enumerators:

enum { one = 1, two = 2, three };

With integral enum types, the compiler will continue counting from the last value, so the enumerator three will get the value 3.

In the StringStack.cpp example above, the line:

static const int size = 100;

would be instead:

enum { size = 100 };

Although you’ll often see the enum technique in legacy code, the static const feature was added to the language to solve just this problem and it produces a more flexible compile-time constant inside a class.

const objects & member functions

Class member functions can be made const. What does this mean? To understand, you must first grasp the concept of const objects.

A const object is defined the same for a user-defined type as a built-in type. For example:

const int i = 1;
const blob b(2);

Here, b is a const object of type blob. Its constructor is called with an argument of two. For the compiler to enforce constness, it must ensure that no data members of the object are changed during the object’s lifetime. It can easily ensure that no public data is modified, but how is it to know which member functions will change the data and which ones are “safe” for a const object?

If you declare a member function const, you tell the compiler the function can be called for a const object. A member function that is not specifically declared const is treated as one that will modify data members in an object, and the compiler will not allow you to call it for a const object.

It doesn’t stop there, however. Just claiming a member function is const doesn’t guarantee it will act that way, so the compiler forces you to reiterate the const specification when defining the function. (The const becomes part of the function signature, so both the compiler and linker check for constness.) Then it enforces constness during the function definition by issuing an error message if you try to change any members of the object or call a non- const member function. Thus, any member function you declare const is guaranteed to behave that way in the definition.

To understand the syntax for declaring const member functions, first notice that preceding the function declaration with const means the return value is const, so that doesn’t produce the desired results. Instead, you must place the const specifier after the argument list. For example,

//: C08:ConstMember.cpp
class X {
  int i;
public:
  X(int ii);
  int f() const;
};

X::X(int ii) : i(ii) {}
int X::f() const { return i; }

int main() {
  X x1(10);
  const X x2(20);
  x1.f();
  x2.f();
} ///:~

Note that the const keyword must be repeated in the definition or the compiler sees it as a different function. Since f( ) is a const member function, if it attempts to change i in any way or to call another member function that is not const, the compiler flags it as an error.

You can see that a const member function is safe to call with both const and non- const objects. Thus, you could think of it as the most general form of a member function (and because of this, it is unfortunate that member functions do not automatically default to const). Any function that doesn’t modify member data should be declared as const, so it can be used with const objects.

Here’s an example that contrasts a const and non- const member function:

//: C08:Quoter.cpp
// Random quote selection
#include <iostream>
#include <cstdlib> // Random number generator
#include <ctime> // To seed random generator
using namespace std;

class Quoter {
  int lastquote;
public:
  Quoter();
  int lastQuote() const;
  const char* quote();
};

Quoter::Quoter(){
  lastquote = -1;
  srand(time(0)); // Seed random number generator
}

int Quoter::lastQuote() const {
  return lastquote;
}

const char* Quoter::quote() {
  static const char* quotes[] = {
    "Are we having fun yet?",
    "Doctors always know best",
    "Is it ... Atomic?",
    "Fear is obscene",
    "There is no scientific evidence "
    "to support the idea "
    "that life is serious",
    "Things that make us happy, make us wise",
  };
  const int qsize = sizeof quotes/sizeof *quotes;
  int qnum = rand() % qsize;
  while(lastquote >= 0 && qnum == lastquote)
    qnum = rand() % qsize;
  return quotes[lastquote = qnum];
}

int main() {
  Quoter q;
  const Quoter cq;
  cq.lastQuote(); // OK
//!  cq.quote(); // Not OK; non const function
  for(int i = 0; i < 20; i++)
    cout << q.quote() << endl;
} ///:~

Neither constructors nor destructors can be const member functions because they virtually always perform some modification on the object during initialization and cleanup. The quote( ) member function also cannot be const because it modifies the data member lastquote (see the return statement). However, lastQuote( ) makes no modifications, and so it can be const and can be safely called for the const object cq.

mutable: bitwise vs. memberwise const

What if you want to create a const member function, but you’d still like to change some of the data in the object? This is sometimes referred to as the difference between bitwise const and memberwise const. Bitwise const means that every bit in the object is permanent, so a bit image of the object will never change. Memberwise const means that, although the entire object is conceptually constant, there may be changes on a member-by-member basis. However, if the compiler is told that an object is const, it will jealously guard that object to ensure bitwise constness. To effect memberwise constness, there are two ways to change a data member from within a const member function.

The first approach is the historical one and is called casting away constness. It is performed in a rather odd fashion. You take this (the keyword that produces the address of the current object) and cast it to a pointer to an object of the current type. It would seem that this is already such a pointer. However, inside a const member function it’s actually a const pointer, so by casting it to an ordinary pointer, you remove the constness for that operation. Here’s an example:

//: C08:Castaway.cpp
// "Casting away" constness

class Y {
  int i;
public:
  Y();
  void f() const;
};

Y:: Y() { i = 0; }

void Y::f() const {
//!    i++; // Error -- const member function
    ((Y*)this)->i++; // OK: cast away const-ness
}

int main() {
  const Y yy;
  yy.f(); // Actually changes it!
} ///:~

This approach works and you’ll see it used in legacy code, but it is not the preferred technique. The problem is that this lack of constness is hidden away in a member function definition, and you have no clue from the class interface that the data of the object is actually being modified unless you have access to the source code (and you must suspect that constness is being cast away, and look for the cast). To put everything out in the open, you should use the mutable keyword in the class declaration to specify that a particular data member may be changed inside a const object:

//: C08:Mutable.cpp
// The "mutable" keyword

class Z {
  int i;
  mutable int j;
public:
  Z();
  void f() const;
};

Z::Z() { i = j = 0; }

void Z::f() const {
//! i++; // Error -- const member function
    j++; // OK: mutable
}

int main() {
  const Z zz;
  zz.f(); // Actually changes it!
} ///:~

This way, the user of the class can see from the declaration which members are likely to be modified in a const member function.

ROMability

If an object is defined as const, it is a candidate to be placed in read-only memory (ROM), which is often an important consideration in embedded systems programming. Simply making an object const, however, is not enough – the requirements for ROMability are much more strict. Of course, the object must be bitwise- const, rather than memberwise- const. This is easy to see if memberwise constness is implemented only through the mutable keyword, but probably not detectable by the compiler if constness is cast away inside a const member function. In addition,

  1. The class or struct must have no user-defined constructors or destructor.
  2. There can be no base classes (covered in the future chapter on inheritance) or member objects with user-defined constructors or destructors.
The effect of a write operation on any part of a const object of a ROMable type is undefined. Although a suitably formed object may be placed in ROM, no objects are ever required to be placed in ROM.


[32] If your compiler doesn’t support static const , you’ll have to replace all the instances in this book of static const inside classes with the enum hack in order to get them to compile.

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]