Mar 1, 1997 12:00 AM PT

Java Tip 27: Typesafe constants in C++ and Java

Let your compiler check that constant values
passed as parameters are valid

This article offers an alternative to using enums or const ints to help eliminate range checking and runtime errors -- first looking at C++ and then exploring an implementation of the same using Java. The summary above shows one of the problems associated with using these constructs. Another problem is the additional range checking that is required to validate the parameter (see below).

Probably the best known and most widely used approach in C++ is through the use of enum.

class Car

{ public:

enum{FORD,VW,SAAB,MAX_CAR}; }; void car_wash(int car)

{

//requires range checking on Cars

if(car>=0 && car<MAX_CAR)

{

//ok

}

} void main(void)

{

car_wash(Car::FORD);

}

The main problem with the approach above is that any int value or object with a conversion to int (that is, a class with operator int()) can be passed to car_wash(). For example, there is nothing stopping someone from using car_wash() like this: car_wash(100); -- or someone who knows that Car::FORD is zero from being lazy and calling car_wash(0). This leaves the implementor of car_wash() with the responsibility of range-checking the parameter.

The next example takes advantage of the C++ compiler's type checking, but it is still open to abuse through the use of a cast.

class Car

{ public:

enum Type{FORD,VW,SAAB}; }; void car_wash(Car::Type c)

{

//do something with c

} void main(void)

{ Car::Type car=Car::FORD;

car_wash(car); //OK car_wash is expecting a Car::Type parameter

car_wash(1); // error, no conversion from int Car::Type

}

Even though Car::Type is an enum and therefore ultimately is an int, the compiler stops clients of car_wash() from passing an int as a parameter. However, although this approach is an improvement on the previous example, it is still open to abuse by undermining the compiler's type checking through the use of a cast. For example, the compiler can not prevent someone from casting an int to a Car::Type.

void main(void)

{ Car::Type car=(Car::Type)100;

car_wash(car);

}

Now this leaves us back at square one. Again a cautious implementor of car_wash() will feel obliged to do some form of range checking.

NOTE: The class keyword can be replaced by namespace in these C++ examples if your compiler supports it and the enums could also be replaced by const int. But the problem we are trying to eliminate remains the same: the need for range checking and the potential for runtime errors.

Below I offer an alternative to the above methods that gives both type safety and "constness."

class Car

{ private: //private ctor to stop instantiation of client Car objects

Car() {}

public:

static const Car FORD;

static const Car VW;

static const Car SAAB; int operator==(const Car &car) const

{

return &car==this;

} }; //static initialization const Car Car::FORD;

const Car Car::VW;

const Car Car::SAAB; void car_wash(const Car &car)

{ if(Car::FORD==car)

{

cout << "The Car being washed is a FORD" << endl;

}

else if(Car::VW==car)

{

cout << " The Car being washed is a VW" << endl;

}

//

} void main(void)

{

car_wash(Car::VW);

}

Notice the use of the private ctor (constructor) this stops the instantiation of client Car objects. Because clients of class Carcan not instantiate their own objects, the need to perform any range checking in car_wash() is eliminated.

One last area of improvement would be to remove the nasty if, else if clauses from car_wash(). The presence of these clauses is a common problem with enumerated types or constants. Although client code becomes more readable through the use of constant objects, the class methods that receive these objects as parameters can become giant switch statements or a series of if, else if clauses.

In a future tip I will explore various techniques that can be used to eliminate these problems.

In the meantime I will leave it as an exercise for the reader to come up with an alternative design for car_wash()that removes the if, else if clauses.

Finally, for those of you who have been patient enough to wade through C++ desperate to get your hands on some Java (and who can blame you!), here is the Java implementation of the Car class. The AWT (abstract windowing toolkit) uses a similar approach in some classes.

public final class Car

{

private Car() {}

public static final Car FORD=new Car();

public static final Car VW=new Car();

public static final Car SAAB=new Car();

}

Notice that in this case, as in many others, the Java implementation is more elegant than the C++ implementation. Also note the use of "final" in the class declaration. The "final" keyword declares that a class can not be subclassed.

At this point some of you may be thinking that the declaration of class Car as final may be too restrictive and that the class should be non-final with a protected ctor to allow subclasses to add new cars. Although not an award-winning design, the Java code below illustrates how the subclassing of class Car can reintroduce the problems we have been trying to eliminate.

import java.util.Vector; public class Car

{

protected Car() {}

public static final Car FORD=new Car();

public static final Car VW=new Car();

public static final Car SAAB=new Car();

} public class FrenchCar extends Car

{

public static final Car CITROEN=new Car();

protected FrenchCar()

{

}

} public class CarWash

{

public void Start(Car car)

{

//

}

} public class SuperCarWash extends CarWash

{

public void Start(Car car)

{

if(FrenchCar.CITROEN==car)

{

//do some specialized washing

//could still call super.Start(car)

//to finish off

}

else

super.Start(car);

}

} public class SuperCarWashOwner

{

private SuperCarWash superCarWash= new SuperCarWash();

private Vector carsToWash = new Vector(); public void AddCar(Car c)

{

carsToWash.addElement(c);

}

public void WashCars()

{

//iterate cars an call superCarWash.Start

}

} public class Test

{

private SuperCarWashOwner scwOwner = new SuperCarWashOwner(); public void TestMethod()

{

scwOwner.AddCar(Car.VW);

//add more cars

scwOwner.WashCars();

}

}

The first thing to note in the code above is that the potential exists for clients of CarWash to pass a FrenchCar to CarWash.Start(). To improve matters, CarWash.Start() could throw an exception if a FrenchCar object is passed in, but all this leads us back to where we started -- trying to design typesafe constants!

It is left as an exercise for the reader to explore this last technique and balance the advantages with the tradeoffs.

Philip Bishop is technical director and a consultant at Eveque Systems Ltd. in the UK. Eveque specializes in designing and implementing distributed object systems using Java, CORBA, C++, ODBMS, and so on. He can be reached at phil@eveque.demon.co.uk.