I was once asked "Why make a class if you are not using inheritance or polymorphism?" At the time I answered: "Modularity." These days I would probably answer "Abstraction and Encapsulation." The latter are often cited as characteristics of object-oriented programming along with inheritance and polymorphism. I will admit that I am not asked questions like this very often, but as a consultant I see a lot of code that looks like it was written by people who apparently think like this questioner. Obviously, another answer is provided by this column: by making something into a class, you can provide operations that otherwise are difficult or even impossible. Beyond all the typical reasons, there is an additional reason to turn common everyday value types into classes: to make them harder to use. Before I explain that, permit me a brief digression.
Once upon a time, right out of college, I was working for NASA on one of the earliest Space Shuttle trainers. (In NASA's case, a trainer was a stripped down simulator it didn't move or provide full systems functionality.) I noticed that all the instrumentation on the flight deck was in traditional American English format (altitude in feet, etc.). Since I thought the Shuttle was suppose to have an international clientele (and because I was young and naïve), I asked one of the instructors why the instrumentation was not calibrated in International Standard units. He replied that when things got stressful, pilots responded better if things were in familiar formats. In particular, most of these people had long experience with the traditional displays. That seemed reasonable for American astronauts/pilots, but what about the rest of the crew? In a lot of cases, the computers would have to be able to display values in several possible formats. I realized that the computer didn't care, so I made the arbitrary decision that in my code physical values would be represented in International Standard units. At the time, this was sort of a smug "I'm Standard, even if the rest of the cockpit isn't" kind of decision. In the intervening years I have stuck to that early choice, and in the process, I have come to realize that I intuitively made a wise decision.
There are lots of ordinary values that have lots of different representations. Date is a common example. What does "01/02/03" mean? If you are from America, you read it one way (2-Jan-2003), in Europe you read it another (1-Feb-2003), and on a computer perhaps still a third way (3-Feb-2001). Is a temperature of 25 warm or cold? The computer doesn't know or care how a number is interpreted, so it is up to the programmers. Unfortunately programmers, being the type of people they are, usually pick the representation easiest to use and most familiar to themselves. Even if they know that the user expects something else, they may figure that is the problem of those UI (User Interface) yahoos. Of course, the UI yahoos figure that the Internal bozos know what the user wanted, so that is what they expect. These kinds of misunderstandings can happen within a project, so how much more likely are they for multi-team, or even multi-national, development efforts? Frankly, I think it would be a shame if a spacecraft were lost (for example) because one group of programmers thought feet-per-second was the "right" way to represent velocity, where another group expected "kilometers-per-hour" (figure out the conversion factor between those scales and decide if you think you would spot the discrepancy).
Back in my FORTRAN days, the best I could do was choose to represent values in scales that were not what might be expected. That way it was a lot harder to forget a conversion. These days in C++, I can do better. If you look at the interface for Temperature in Listing 1, you will note several things. First, let me point out the absence of a user-defined copy constructor, copy assignment operator, and destructor. This makes the class what I call a Light Weight Class [5]. On a decent compiler, this means that copy operations use bitwise copy, and exception stack unwind ignores objects of this type. Beyond that, notice that there is a public default constructor, but that the converting constructor is protected (more on this in a moment).
The reason for this is found in the adage "good classes should be easy to use correctly, and hard to use incorrectly." Unfortunately, in the real world, there is often a tension between ease of use and ease of misuse. I decided to give up some ease of use in return for making my class harder to use incorrectly. The absence of a converting constructor means you can not initialize a Temperature object from an ordinary number. Instead, you have to use one of the provided static functions. For example:
Temperature t1 = Temperature::inC(25); // warm Temperature t2 = Temperature::inF(25); // coldLikewise, to extract the value from a Temperature object, you have to write something like:
float x = t.inK();This makes the code harder to write initially, but much more explicit when it is read later. It also means that the code is more likely to be correct since the programmer doesn't have to worry about the conversion formula. Note: I am not overly crazy about the function names myself, but they are easier to write than Celsius and Fahrenheit and seem to work all right in practice. A quick glance at the functions will show that Temperature is stored internally as Kelvin, but it really doesn't matter.
The converting constructor is protected instead of private to allow for derived classes. Since there are no virtual functions in particular no virtual destructor this may seem a little peculiar. (Note that you should not allocate a class derived from Temperature from the free store and then delete it through a Temperature* that yields undefined behavior). Deriving from Temperature is useful in two cases (that I know of anyway). First, if the application deals with only one format (or the programmer is lazy and just hates to write code like the above), it is possible to trivially create a derived class that represents a temperature in a single scale:
class Celsius : public Temperature { public: Celsius() {} Celsius(float val) : Temperature(inC(val)) {} explicit Celsius(const Temperature& other) : Temperature(other) {} Celsius& operator=(float val) { Temperature::operator=(inC(val)); return *this; } float get() const { return inC(); } };Since Celsius is publicly derived from Temperature, all the I/O operations described for Temperature will also work for an object of type Celsius, including being able to display its value in another scale. If this is considered undesirable, then an operator<< function can be provided explicitly for Celsius that only outputs the value in that scale.
Alternativly, if a user wishes to deal with another temperature scale than those provided (e.g., Rankin), then a derived class can provide the necessary conversions:
class Rankin : public Temperature { public: Rankin() {} explicit Rankin(float val) : Temperature(inK(val * 5 / 9)) {} explicit Rankin(const Temperature& other) : Temperature(other) {} float get() const { return inK() * 9 / 5; } };In this case, the converting constructor is public but marked explicit. Typically, you would use Rankin like so:
Temperature t = Rankin(300.0); float val = Rankin(t).get();If you want to output Rankin temperature values to an ostream, the functionality described in the article can be extended by simply adding the necessary manipulators and format functions to class Rankin.