User Guide for Units Scalar Class Library

Introduction

A scalar is a one-dimensional real number that represents a single measured value, such as length, time, or mass. No reference is assumed for the measured value, and a negative measured value is just as valid as a positive one. The application determines the context of the measurement and the validity of it.

Typically, software is written with implicit units for measurements. That is, a double or float variable is declared and assigned a number which is implied to be in a particular unit of measure. For example:

      double length = 5;  // meters
This practice can be very dangerous because unit mismatches cannot be detected at compile time. For instance, if a function is declared to take a length in feet, we could accidentally pass it meters and not be aware of the error until run-time. Even worse, we could just as easily pass a completely different unit of measure. For instance, we could pass 10 seconds to a function expecting a length in meters.

The basic idea of the scalar class library is to abstract the idea of a unit of measure so that C++ code can be written without the pitfalls of implicit units. By declaring a template class that keeps track of the exponents of all basic units of measure, we can use the compiler to detect unit mismatches. The strong type checking of C++ makes it much more difficult (but not impossible) to accidentally mix units. Template arithmetic operators keep track of exponent manipulation so that proper units of measure are enforced even after complex arithmetic combinations of units of measure.

The main scalal class is the Unit class template that encodes the exponents of the fundamental SI units of measure (mass, length, time, current, temperature, amount, intensity, angle):

template < typename ValueType_,
           int massExp_,
           int lengthExp_,
           int timeExp_,
           int currentExp_,
           int temperatureExp_,
           int amountExp_,
           int intensityExp_,
           int angleExp_ >
class Unit;
However, using this class can be unwieldy, so some convenience typedefs for double values are provided (Length, Time, Speed, etc.). The full list of scalar typedefs is here. You can be more explicit that you want a float or double valued type by prefixing the type name with "f" or "d" (e.g. fLength or dLength).


Construction

Great effort was taken to minimize the overhead associated with using the scalar classes. Since operating on measured values is much more common than construction and initialization, the scalar Unit class template is designed to have a fixed internal representation. This internal representation is hidden from the application so that it can change without affecting anything else. Because of this, a scalar class takes up the same memory as a double value.

Units must be specified with any value assigned to a measurement variable. For example,

      Length length = 5;                 // error
      Length length = MetersLength(5);   // OK
A default constructor is provided so that a value can be assigned later. The default value is undefined (just as with a double) because assuming a default would introduce unwarrented overhead. Example:
      Length length;  // UNINITIALIZED!
      length = MetersLength(5);
Some convenience special types are provided for representing zero, positive infinity, or negative infinity in any unit of measure:
      Length length1 = zero;  // equivalent to: = MetersLength(0)
      Length length2 = infinity;
      Length length3 = negInfinity;

Once a unit of measure is created, it's identity will prevent it from being used when a function is expecting different units. For example:

      extern void doSomething(Length &length);

      doSomething(SecondsTime(5));    // error
      doSomething(MetersLength(5));   // OK
      doSomething(FeetLength(5));     // OK

You should not be concerned about construction overhead associated with constant values. For instance:

      if (angle < DegreesAngle(45))
      {
      }
Some application programmers might be concerned that the DegreesAngle constructor will incur unwanted conversion overhead every time this test is performed. They will instead attempt to use the internal units to avoid the conversion:
      if (angle < RadiansAngle(0.78539816))
      {
      }
For one thing, this is a dangerous assumption since the internal representation is hidden and could potentially change. Further, since most people don't think in radians, this makes the code difficult to read and understand. The first method should be used without concern because most compilers will optimize constant arithmetic out so no overhead is incurred at run-time.


Comparison

Testing for equality can be done in two ways: binary and similar. Binary equality is the same as testing double == double. This is potentially dangerous because it compares the binary images of the two numbers, not their values. The slightest variation can lead to an inequality. It's up to the application to decide if this is the desired behavior.

More often, the application will wish to test if two measurements are "close enough" to each other. This can be done with the similar function. This uses a tolerance for determining how close two values need to be to be considered similar. For example:

      if (similar(MetersLength(2), MetersLength(3), MetersLength(0.1)))
      {
         // Will never get executed.
      }

      if (similar(MetersLength(2), MetersLength(2.5), MetersLength(1)))
      {
         // This is true.
      }

Inequality operators are provided. You can mix subclasses of the same base unit of measure to make things readable. For example:

      if (MetersLength(1) > FeetLength(1))
      {
         // This is true.
      }
      if (SecondsTime(1) < HoursTime(1))
      {
         // This is true.
      }
Inequality operators do not use tolerances.


Arithmetic

Once a unit of measure has been constructed, it can be manipulated in many ways just like a double variable. Some examples are:

      Length a = MetersLength(5);

      Length b = 2 * a / 3.4;

      b += FeetLength(1.2);  // OK
      b += 1.2;              // error

      Length c = (a - b);
Note that the unit type is preserved in arithmetic. That means you can mix different specific units of the same base type:
      Length a = (MetersLength(5) + FeetLength(2));   // OK
      Length a = (MetersLength(5) + SecondsTime(2));  // error
Most units will cancel themselves by division. In other words, the ratio of two Lengths is a double:
      Length a = MetersLength(5);
      Length b = MetersLength(5);

      double ratio = a/b;


Cross-Unit Arithmetic

The Unit clas template defines all arithmetic operators so that variables with different units of measure can be arbitrarily combined in a type-safe manner. So for instance,

Speed = Length / Time
Acceleration = Speed / Time
Force = Mass * Acceleration
AngularSpeed = Angle / Time
Length = Angle * Length
Area = Length * Length
Volume = Area * Length
Density = Mass / Volume
MassFlowRate = Mass / Time
Frequency = double / Time
Pressure = Force / Area

These relationships can be rearranged to suit the application. For instance:

      // Compute speed.
      Speed speed = MetersLength(5) / SecondsTime(2);

      // Compute distance traveled after a duration.
      Length distance = speed * SecondsTime(10); 

      // Compute time to travel a distance.
      Time time = speed / FeetLength(23);

You can declare a variable with an arbitrary unit of measure by using the Unit class template:

      typedef Unit< double, -1, 3, -2, 0, 0, 0, 0, 0 > GravitationalConstantUnits;
      // Gravitational constant
      GravitationalConstantUnits G = MetersVolume(6.67300e-11)/(KilogramsMass(1)*SecondsTime(1)*SecondsTime(1));

      typedef Unit< double, 0, -1, 0, 0, 1, 0, 0, 0 > TemperaturePerLength;
      TemperaturePerLength thermalLapseRate = CelsiusTemperature(0.006506986)/MetersLength(1);


Unit Conversion

Because the internal representation is fixed, conversion between units take place in the constructor. Most applications should then deal only with the base types. However, there are times when an application must get a double representing the current value. For instance, you might need to pass a double value to a third-party library. To do this, use the value() member function on a specific unit instance:

      void wrapper(Length const & length)
      {
          double meters = MetersLength(length).value();
          call_function(meters);
      }


Scalar Functions

The following functions are provided for any unit of measure type:

      sqrt
      abs
      min
      max
So for instance, sqrt(FeetArea(16)) == FeetLength(4)


Output Formatting

There are times when the application must present a value to the user. Since the user needs to see the value in a particular units, the units must be specified by the application. An ostream operator is provided for all subclasses which prints the value followed by the units abbreviation. For example, the following prints "2.3 ft":

      cout << FeetLength(2.3) << endl;

For some unit types, there are additional formats which are not available as subclasses. For instance, Speed has several common combinations of Length and Time types, but certainly not all of them. In these cases, you can use the format template classes. For instance, if we wanted to print a speed in miles per minute, we would use the following:

      Speed speed = KnotsSpeed(400);
      cout << SpeedFormat< StatuteMilesLength,
                           MinutesTime >(speed) << endl;

Note that the following will print slightly different things:

      Speed speed = KnotsSpeed(400);
      cout << SpeedFormat< NauticalMilesLength,
                           HoursTime >(speed) << endl;

      Speed speed = KnotsSpeed(400);
      cout << KnotsSpeed(speed) << endl;
The first will print "400 nm/hr", while the second will print "400 kts".

For arbitrary units of measure, you can use the Format class template which allows you to specify the specific fundamental units of measure. Or more typically, you would use one of the two typedefs: MksFormat for Meters-Kilograms-Seconds or CgsFormat for Centimeters-Grams-Seconds. For example, this prints "6.673e-11 m^3/kg-s^2".

      cout << MksFormat(G) << endl;


Special Types

While most of the unit types are fairly straight-forward in usage, there are a few notable types which warrent special consideration: Temperature and Angle.

Temperature

There are two types of temperatures: relative and absolute. The generic Temperature class is relative and can be used in arbitrary arithmetic and combined with other units of measure (i.e., Temperature/Voltage). AbsTemperature has an offset (for instance, Kelvin = Celsius + 273) and therefore cannot be scaled or combined with other units of measure. So the following arithmetic operations are not defined for AbsTemperature: scaling, negation, and ratio. Doubling a Kelvin value gives quite a different temperature than doubling a Celsius value! You can use relative and absolute temperatures together like this:

      AbsTemperature a = AbsCelsiusTemperature(23);
      AbsTemperature b = a*2;  // error
      Temperature deltaT = CelsiusTemperature(4.5);
      deltaT *= 2;  // OK
      AbsTemperature b = a + deltaT;

Angle

Mathematically, there are two types of angles: flat and circular. A flat angle can have any value and has no concept of wrapping around. In other words, with a flat angle, 0 != 360 degrees. This is useful in representing how far a shaft has rotated, for instance. The Angle class is a flat angle and gives unconstrained representation of a measure of angle.

A circular angle is one where values wrap around; 0 == 360 degrees. There are two popular references for circular angles: one that goes from -180 to 180 degrees and one that goes from 0 to 360 degrees. These are the SignedAngle and UnsignedAngle classes, respectively. Both will wrap when testing for similarity. For instance:

      if (similar(SignedDegreesAngle( 179),
                  SignedDegreesAngle(-179), DegreesAngle(3))
      {
         // This is true.
      }
      if (similar(UnsignedDegreesAngle(  1),
                  UnsignedDegreesAngle(359), DegreesAngle(3))
      {
         // This is true.
      }
Subtracting two circular angles will always return the smallest angle between the two angles. For instance:
      (SignedDegreesAngle(-170) -
       SignedDegreesAngle( 170)   == DegreesAngle( 20)

      (SignedDegreesAngle( 170) -
       SignedDegreesAngle(-170))  == DegreesAngle(-20)

      (UnsignedDegreesAngle( 10) -
       UnsignedDegreesAngle(350)   == DegreesAngle( 20)

      (UnsignedDegreesAngle(350) -
       UnsignedDegreesAngle( 10)   == DegreesAngle(-20)

All angle classes can be used with trig functions. For instance:

      cos(DegreesAngle( 90)) ==  0
      sin(DegreesAngle(-90)) == -1
      tan(DegreesAngle( 45)) ==  1
      arccos( 0) == DegreesAngle( 90)
      arcsin(-1) == DegreesAngle(-90)
      arccos( 1) == DegreesAngle( 45)
All angle classes can be multiplied by a Length radius to get the arc-length:
      (DegreesAngle(180) * MetersLength(1)) == MetersLength(M_PI)